Client 인증서의 유효 기간
+기본 설정시 1,209,600초(2주)의 유효 기간을 갖게 되며, 설정에 따라 긴 유효시간의 적용이 가능합니다. (옵션 : deault_tls_client_ttl
)
+설정은 상기 도식화한 절차 중 "2. kmip 기본 config" 단계에 적용 가능하며. 이는 KMIP 적용 흐름도
의 "4. kmip scope, role 정의" 단계에서 override 할 수 있습니다.
diff --git a/00-Howto/01-Overview.html b/00-Howto/01-Overview.html new file mode 100644 index 0000000000..a2f63a36d2 --- /dev/null +++ b/00-Howto/01-Overview.html @@ -0,0 +1,61 @@ + + +
+ + + + + + +문서가 인터넷상에 공개되는 목적은 접근성을 극대화 하기 위함 입니다. 또한 로컬환경에서 빠르게 문서를 검색하기 위해 해당 git repo를 clone 받거나 download 받아서 별도의 마크다운 툴과 연동하는 것도 가능합니다. VuePress
기반으로 구성되었기 때문에 이외의 방식은 문서 표기에 제약이 있을 수 있습니다.
docmoa의 공개된 페이지를 통해 문서를 읽을 수 있습니다.
공개된 페이지는 내 로컬환경에서도 실행할 수 있습니다. Nodejs가 필요합니다.
# git clone
+git clone https://github.com/docmoa/docs.git
+
+# npm install
+cd docs
+npm install
+
+# start VuePress writing
+npm run dev
+
# git clone
+git clone https://github.com/docmoa/docs.git
+
+# npm install
+cd docs
+yarn install
+
+# start VuePress writing
+yarn vuepress dev
+
실행이 완료되면 로그에 다음과 같은 메시지와 접속할 수 있는 링크가 나타납니다.
success [10:48:28] Build 6f9dd7 finished in 1179 ms! ( http://localhost:8000/ )
+
웹브라우저에서 실행시 표기되는 로그의 링크를 입력하면 공개된 웹화면과 동일한 환경을 확인할 수 있습니다.
typora
는 멀티 OS를 지원하는 마크다운 에디터/뷰어 입니다. VuePress의 플러그인과 일부 호환되지 않는 표기들이 있으나, 전역 검색이 가능하고 개인 노트를 활용하듯 관리할 수 있습니다.
git clone https://github.com/docmoa/docs.git
+
3.typora
를 실행하고 옵션에서 [파일] > [열기] 를 클릭하여 앞서 받은 소스 디렉토리의 docs
디렉토리를 열어줍니다.
Visual Studio Code
에서는 마크다운의 프리뷰를 지원합니다. 앞서 typora는 마크다운으로 작성하는 즉시 마크다운 형태로 변경되었다면, VS Code
에서는 좌우 비교하면서 작성한 표현이 어떻게 반영되는지 확인할 수 있습니다.
git clone https://github.com/docmoa/docs.git
+
VS Code
를 실행하고 [파일] > [열기] 를 클릭하여 앞서 받은 소스 디렉토리의 docs
디렉토리를 열어줍니다.Preview
버튼을 클릭하면 양쪽으로 비교하면서 글을 확인/편집 할 수 있습니다.docmoa에 문서 기여하기위한 가이드를 설명합니다.
팁
다양한 방법으로 문서를 작성하고 기여할 수 있습니다.
얽매이지 마세요.
문서는 모두 git으로 관리되며 공개되어있습니다. 문서 기여를 위한 방식은 별도 안내로 구분하여 설명합니다.
git clone https://github.com/docmoa/docs.git
+
또는 github에서 fork하여 별도 관리 후 pull request 하여도 좋습니다.
clone 받은 구조는 VuePress의 구조를 갖고 있습니다. 문서의 기준이 되는 디렉토리는 docs
입니다.
디렉토리 구조
.
+├── .gitignore
+├── LICENSE.md
+├── README.md
+├── `docs`
+│ ├── .vuepress
+│ ├── 00-Howto
+│ ├── 01-Infra
+│ ├── 02-PrivatePlatform
+│ ├── 03-PublicCloud
+│ ├── 04-HashiCorp
+│ ├── 05-etc
+│ ├── 98-tag
+│ ├── 99-about
+│ └── README.md
+└── package.json
+
브라우저에서 보여지는 화면을 실시간으로 확인하기 위해 로컬환경에서 Vewpress를 실행합니다. Nodejs가 필요합니다.
# 클론 받은 디렉토리 이동 후 npm install
+cd docs
+npm install
+
+# start VuePress writing
+npm run dev
+
# 클론 받은 디렉토리 이동 후 npm install
+cd docs
+yarn install
+
+# start VuePress writing
+yarn vuepress dev
+
실행이 완료되면 로그에 다음과 같은 메시지와 접속할 수 있는 링크가 나타납니다.
실행 후 접속 링크 출력
✔ Initializing and preparing data - done in 12.34s
+
+ vite v4.4.9 dev server running at:
+
+ ➜ Local: http://localhost:8080/
+ ➜ Network: http://192.168.0.9:8080/
+
웹브라우저에서 실행시 표기되는 로그의 링크를 입력하면 공개된 웹화면과 동일한 환경을 확인할 수 있습니다.
docs
디렉토리 내에 기존에 구성된 항목 내에 작성도 가능하고 새로운 카테고리를 생성할 수도 있습니다. 디렉토리와 파일은 생성된 순서와 관계없이 정렬되기 때문에 좌측 Sidebar
표기시 원하는 순서대로 표기되기를 원하는 경우 {숫자}-
을 파일명 앞에 붙여 의도한대로 표시되도록 구성가능합니다.
예를 들어 Howto
에 작성된 내용을 바탕으로 설명합니다.
문서 트리
00-Howto
+├── `01-Overview.md`
+├── `02-Guide`
+│ ├── 01-Start.md
+│ ├── 02-PullRequest.md
+│ └── 03-Fork.md
+├── `03-Tips`
+│ ├── CodeBlock.md
+│ ├── Link.md
+│ └── TipBox.md
+└── `README.md`
+
index.html
과 같이 해당 디렉토리의 기본 페이지로 등록됩니다. 디렉토리 경로만 입력하는 경우 해당 페이지가 나타납니다. https://docmoa.github.io/00-Howto/
https://docmoa.github.io/00-Howto/README.md
디렉토리 이름
디렉토리 이름에 공백이 있는 경우 다른 문서에서 참조 시 공백 문자인 %20
를 표기해야 하는 이슈가 발생할 수 있습니다.
[](https://docmoa.github.io/00-Howto/공백이%20있는%20디렉토리)
+
여기서는 SSH Too many authentication failures
와 관련한 간단한 글을 개시하는 것을 예로 설명하겠습니다.
실제 VuePress 환경으로 어떤 내용이 표기되는지 동적인 확인을 위해 앞서 기존 환경을 로컬에서 실행하고 확인하기에서 처럼 화면을 띄우고 작업하면 실제 작성하는 글들이 어떻게 보여지는지도 함께 확인할 수 있습니다.
Linux
카테고리로 가정하고 01.Infra > Linux > TroubleShooting
이라는 디렉토리를 생성합니다.
문서 작성을 위한 SSH Too many authentication failures.md
파일을 생성합니다.
Title
이 메뉴에 표기됩니다.문서 내용에는 문서 기본 서문(Frontmatter)을 작성하기 위한 ---
로 구성된 블록이 최상단에 명시됩니다. 내용은 기존 마크다운 문서를 작성하는 것과 동일하게 구성합니다.
---
+
+---
+
+# SSH Too many authentication failures (제목인 Title은 h1 스타일로)
+
+너무많은 인증 실패로 인한 SSH 접속이 안되는 메시지를 간혹 보게되는 경우가 있다.
+<생략>
+
작성한 문서를 docmoa에 기여하는 방법은 다음 Contribute 항목에서 설명합니다.
docmoa에 문서 기여하기위한 가이드를 설명합니다. 일반적인 github 상에서의 코드 기여 방식과 동일합니다.
로컬 환경에서 git 명령을 수행하기 위해 설치합니다. github 브라우저 환경에서 수정하는 것도 가능하지만, 로컬에서 문서를 활용하고 오프라인 작업을 위해서는 설치하시기를 권장합니다.
Git 설치 방법 안내를 참고하여 아래 설명합니다.
sudo dnf install git-all
+
sudo apt install git-all
+
Fork
문서는 github상에서 관리됩니다. 우선 문서를 추가구성하고 수정할 수 있도록 원본 github repo를 Fork
합니다.
https://github.com/docmoa/docs
로 이동합니다.Fork
를 클릭하고 나의 github Org를 선택합니다.Fetch
or Pull
Fork의 원본 Repo에 변경에 대해 작업중인 Repo에 변경사항을 적용해야 하는 필요성이 있습니다. 여러 편집자가 동일한 시점에 동일 문서를 편집하게 되면 편집에 충돌이 발생할 수 있습니다.
참고 : https://en.wikipedia.org/wiki/Edit_conflict
충돌을 사전에 최대한 방지하기 위해서 편집 전에 원본의 문서를 가져오고 병합하는 과정이 필요합니다.
CLI 컨트롤을 위해서는 앞서 git 유틸 설치가 필요합니다.
# 1. Fork 받은 본인 소유의 Repo를 Clone 받습니다.
+git clone https://github.com/docmoa/docs
+
+# 2. 해당 소스 디렉토리로 이동하여 remote 를 확인합니다.
+cd docs
+git remote -v
+origin https://github.com/myorg/docs.git (fetch)
+origin https://github.com/myorg/docs.git (push)
+
+# 3. 문서 원본 Repo와의 병합을 위해 `upstream` repo remote를 추가합니다.
+git remote add upstream https://github.com/docmoa/docs
+
+# 4-1. pull 을 수행하거나
+git pull upstream main
+
+# 4-2. fetch & merge 를 수행합니다.
+git fetch upstream
+git merge upstream/main
+
원본 Repo에 변화가 있으면 UI상에서 상태가 알려집니다. 이 경우 우측의 Fetch upstream
을 통해 Compare
로 변경사항을 확인하거나 Fetch and merge
로 현재의 Repo에 병합할 수 있습니다.
add
and commit
문서 작성은 앞서 문서작성 '시작'을 참고하세요. 문서 또는 문서 작성에 필요했던 이미지 등 준비가 끝나면 해당 파일을 본인 소유의 Repo에 추가합니다. 이 때 사용하는 것은 CLI도 가능하고, UI 기반에서 작성한 경우에는 저장 즉시 해당 Repo에 저장됩니다. Commit의 경우 필요에 따라 브랜치를 별도 생성하여 본인의 Repo를 기준으로 관리하는 것 또한 가능합니다.
CLI 컨트롤을 위해서는 앞서 git 유틸 설치가 필요합니다.
# 1. 작성한 파일을 git의 관리 대상으로 add 합니다.
+git add path/문서.md
+
+# 2. 로컬 Repo에서 변경사항을 Commit 합니다.
+git commit -m "커밋메시지를 남깁니다.(예를들어 : 문서를 수정)"
+
+# 3. 원격 Repo로 변경사항을 Push 합니다.
+git push origin main
+
github 웹 환경에서 문서를 추가하고 수정하는 것도 가능합니다.
최종적으로 Fork 한 Repo에 docmoa에 기여할 문서가 준비가 된경우 해당 Repo의 github ui에서 커밋된 정보가 있다는 문구와 Contribute
를 클릭하여 Pull request
를 진행 할 수 있습니다. Open pull request
를 클릭하여 Upstream Repo에 요청을 보냅니다.
Pull request를 생성하면 본인 소유의 Repo(Branch)로 부터 docmoa에 지금까지의 변경사항을 병합할 것을 요청할 수 있습니다.
팁
상단에 표기되는 repo의 방향과 branch를 다시한번 확인해주세요.
아래 어떤 항목이 어떻게 변경되는지 내용을 확인할 수 있습니다.
Create pull request
버튼을 클릭하면 디테일한 설명을 추가할 수 잇습니다. 문서 자체의 변경사항만으로 의도를 전달하기 힘든경우 해당 설명이 이해하는데 큰 도움이 됩니다.
이제 Pull request가 받아들여지고나면 docmoa에 기여해주신 내용이 반영됩니다.
Build 주기
2021년 10월 4일 기준 매 5분마다 변경사항이 있으면 docmoa의 빌드가 수행됩니다.
docmoa에 문서 템플릿을 설명합니다.
주의
기본 템플릿 가이드를 잘 지켜주세요. 함께 만드는 문서 모음이므로, 기본적인 형식이 필요합니다.
---
+
+---
+
+# h1 제목 = Title 입니다.
+내용은 마크다운 형식으로 작성합니다.
+
+## h2 제목
+
+
---
로 구분되는 내용입니다. 자세한 내용은 공식 가이드를 참고하세요. 여기서 주로 사용하는 내용은 다음과 같습니다. h1
으로 지정되는(#
) 제목이 기본으로 적용됩니다.meta
에 추가 기입되는 정보입니다. 검색 엔진에 활용됩니다.# 제목
: Frontmatter
에서 title
을 명시하지 않는 것이 문서 작성에 복잡함을 줄여줄 것으로 예상되어 둘 중에 h1
스타일의 제목을 넣는 것을 권장합니다.문서 작성시 차트를 추가하는 방법을 안내합니다.
차트 구성 방식은 ChartJS를 따릅니다.
::: chart
와 :::
로 처리합니다.
::: chart A bar chart
+
+```json
+{
+ "type": "bar",
+ "data": {
+ "labels": ["Red", "Orange", "Yellow", "Green", "Blue", "Purple"],
+ "datasets": [{
+ "label": "My First Dataset",
+ "data": [12, 19, 3, 5, 2, 3],
+ "backgroundColor": [
+ "rgba(255, 99, 132, 0.2)",
+ "rgba(255, 159, 64, 0.2)",
+ "rgba(255, 205, 86, 0.2)",
+ "rgba(75, 192, 192, 0.2)",
+ "rgba(54, 162, 235, 0.2)",
+ "rgba(153, 102, 255, 0.2)"
+ ],
+ "borderColor": [
+ "rgb(255, 99, 132)",
+ "rgb(255, 159, 64)",
+ "rgb(255, 205, 86)",
+ "rgb(75, 192, 192)",
+ "rgb(54, 162, 235)",
+ "rgb(153, 102, 255)"
+ ],
+ "borderWidth": 1
+ }]
+ },
+ "options": {
+ "scales": {
+ "y": {
+ "ticks": {
+ "beginAtZero": true,
+ "callback": "function(value){ return '$' + value + 'k'; }"
+ },
+ "beginAtZero": true
+ }
+ }
+ }
+}
+```
+
+
+
::: chart A Bubble Chart
+
+```json
+{
+ "type": "bubble",
+ "data": {
+ "datasets": [
+ {
+ "label": "First Dataset",
+ "data": [
+ { "x": 20, "y": 30, "r": 15 },
+ { "x": 40, "y": 10, "r": 10 }
+ ],
+ "backgroundColor": "rgb(255, 99, 132)"
+ }
+ ]
+ }
+}
+```
+
+
마크다운 기본 사용 법과 거의 동일합니다.
코드블록은 ``` 과 ``` 사이에 코드를 넣어 로 표기합니다. 아래와 같이 md 파일 내에 작성하면
```
+# Code block e.g.
+This is my code
+```
+
다음과 같이 표기됩니다.
# Code block e.g.
+This is my code
+
```
대신 ~~~
도 사용 가능합니다.
VuePress는 Prism Javascript 라이브러리를 통해 키워드 강조 표시를 지원 합니다. 목록에 대한 확인은 components.json 의 languages
를 참고할 수 있습니다.
```python
+print("hello, world.")
+```
+
print("hello, world.")
+
문서 작성 시 코드블록에서 강조하고 싶은 경우 코드블럭의 스타일 에 강조할 라인 번호를 명시합니다.
```python {2,4-5}
+print("nomal")
+print("highlight!")
+print("nomal")
+print("highlight!")
+print("highlight!")
+```
+
print("nomal")
+print("highlight!")
+print("nomal")
+print("highlight!")
+print("highlight!")
+
상황에 따라 동일한 구성 및 동작을 위한 코드블록을 옵션을 주어 선택적으로 표기하고 싶은 경우 <code-group>
과 <code-block>
을 활용 합니다.
::: code-tabs#shell
+@tab Option1
+```bash {2}
+# This is Option 1
+chmod 755 ./file.txt
+```
+
+@tab Option2
+```bash {2}
+# This is Option 2
+chmod +x ./file.txt
+```
+:::
+
# This is Option 1
+chmod 755 ./file.txt
+
# This is Option 2
+chmod +x ./file.txt
+
https://vuepress-theme-hope.github.io/md-enhance/guide/demo/#
::: normal-demo Demo
+
+```html
+<h1>Mr.Hope</h1>
+<p>is <span id="very">very</span> handsome</p>
+```
+
+```js
+document.querySelector("#very").addEventListener("click", () => {
+ alert("Very handsome");
+});
+```
+
+```css
+span {
+ color: red;
+}
+```
+
+:::
+
<h1>Mr.Hope</h1>
+<p>is <span id="very">very</span> handsome</p>
+<button>hello</button>
+
document.querySelector("#very").addEventListener("click", () => {
+ alert("Very handsome");
+});
+
span {
+ color: red;
+}
+
문서 작성시 외부 링크를 포함하는 예를 설명합니다. 참고 문서
설명하던 글의 특정 단어에 대해 외부 링크를 추가하고자 하는 경우 브라킷[ ]
과 괄호를 사용합니다. domain을 같이 기입하는 경우 새창에서 열기로 표기됩니다.
새창으로 이동하는 [링크 달기](http://docmoa.github.io/00-Howto/03-Tips/Link.html)
+현재 창에서 이동하는 [링크 달기](/00-Howto/03-Tips/Link.html)
+
다음과 같이 표기됩니다.
별도의 연결된 단어 없이 링크 자체를 넣는 경우 < >
를 사용합니다.
<https://docmoa.github.io>
+<docmoa@gmail.com>
+
다음과 같이 표기됩니다.
이미지는 !
를 기존 링크 문법 앞에 추가합니다.
![대체 텍스트](이미지 링크 "이미지 설명")
+
+![](https://icons.iconarchive.com/icons/fatcow/farm-fresh/32/layout-link-icon.png)
+![대체 텍스트](https://img.icons8.com/ios/2x/깨진링크)
+![대체 텍스트](https://icons.iconarchive.com/icons/fatcow/farm-fresh/32/report-link-icon.png "이미지 설명")
+
다음과 같이 표기됩니다.
외부 동영상은 html 문법을 활용하여 추가할 수 있습니다. 여기서는 유튜브를 예를 들어 설명합니다. 다른 여러가지 방식은 참고 링크를 확인해주세요.
공유
버튼을 누르고 퍼가기
를 클릭하면 우측에 동영상 퍼가기 코드가 나타납니다.<iframe width="560" height="315" src="https://www.youtube.com/embed/StTqXEQ2l-Y" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
+
다음과 같이 표기됩니다.
markdown-it-plantuml 플러그인을 활성화 하여 UML 작성이 가능합니다. 아래는 플러그인 개발자의 안내를 풀어 일부 설명합니다.
UML 블록은 @startuml
과 @enduml
사이에 UML 구성을 위한 구성을 넣어 표기합니다. 아래와 같이 md 파일 내에 작성하면
@startuml
+Bob -> Alice : hello
+@enduml
+
다음과 같이 표기됩니다.
@startuml
Bob -> Alice : hello
@enduml
다양한 예제는 plantuml.com에서 확인할 수 있습니다.
@startuml
+actor User
+interface Terraform
+cloud CLOUD
+
+User ->> Terraform : Apply
+User <<- Terraform : State
+Terraform ->> CLOUD : Probisioning
+CLOUD ->> Terraform : Response
+@enduml
+
@startuml
actor User
interface Terraform
cloud CLOUD
User ->> Terraform : Apply
User <<- Terraform : State
Terraform ->> CLOUD : Probisioning
CLOUD ->> Terraform : Response
@enduml
http://plantuml.com/sequence-diagram
@startuml
+Alice -> Bob: Authentication Request
+Bob --> Alice: Authentication Response
+
+Alice -> Bob: Another authentication Request
+Alice <-- Bob: another authentication Response
+@enduml
+
@startuml
Alice -> Bob: Authentication Request
Bob --> Alice: Authentication Response
Alice -> Bob: Another authentication Request
Alice <-- Bob: another authentication Response
@enduml
http://plantuml.com/use-case-diagram
@startuml
+:Main Admin: as Admin
+(Use the application) as (Use)
+
+User -> (Start)
+User --> (Use)
+
+Admin ---> (Use)
+
+note right of Admin : This is an example.
+
+note right of (Use)
+ A note can also
+ be on several lines
+end note
+
+note "This note is connected\nto several objects." as N2
+(Start) .. N2
+N2 .. (Use)
+@enduml
+
@startuml
:Main Admin: as Admin
(Use the application) as (Use)
User -> (Start)
User --> (Use)
Admin ---> (Use)
note right of Admin : This is an example.
note right of (Use)
A note can also
be on several lines
end note
note "This note is connected\nto several objects." as N2
(Start) .. N2
N2 .. (Use)
@enduml
http://plantuml.com/class-diagram
@startuml
+Object <|-- Dummy
+
+class Dummy {
+ String data
+ void methods()
+ -field1
+ #field2
+ ~method1()
+ +method2()
+}
+
+class Flight {
+ flightNumber : Integer
+ departureTime : Date
+}
+
+class Car
+
+Driver - Car : drives >
+Car *- Wheel : have 4 >
+Car -- Person : < owns
+@enduml
+
@startuml
Object <|-- Dummy
class Dummy {
String data
void methods()
-field1
#field2
~method1()
+method2()
}
class Flight {
flightNumber : Integer
departureTime : Date
}
class Car
Driver - Car : drives >
Car *- Wheel : have 4 >
Car -- Person : < owns
@enduml
http://plantuml.com/activity-diagram-beta
@startuml
+start
+partition Initialization {
+ :read config file;
+ :init internal variable;
+}
+partition Running {
+ if (multiprocessor?) then (yes)
+ fork
+ :Treatment 1;
+ fork again
+ :Treatment 2;
+ detach
+ end fork
+ else (monoproc)
+ :Treatment 1;
+ :Treatment 2;
+ endif
+}
+
+stop
+@enduml
+
@startuml
start
partition Initialization {
:read config file;
:init internal variable;
}
partition Running {
if (multiprocessor?) then (yes)
fork
:Treatment 1;
fork again
:Treatment 2;
detach
end fork
else (monoproc)
:Treatment 1;
:Treatment 2;
endif
}
stop
@enduml
http://plantuml.com/component-diagram
@startuml
+package "Some Group" {
+ HTTP - [First Component]
+ [Another Component]
+}
+
+node "Other Groups" {
+ FTP - [Second Component]
+ [First Component] --> FTP
+}
+
+cloud {
+ [Example 1]
+}
+
+
+database "MySql" {
+ folder "This is my folder" {
+ [Folder 3]
+ }
+ frame "Foo" {
+ [Frame 4]
+ }
+}
+
+
+[Another Component] --> [Example 1]
+[Example 1] --> [Folder 3]
+[Folder 3] --> [Frame 4]
+@enduml
+
@startuml
package "Some Group" {
HTTP - [First Component]
[Another Component]
}
node "Other Groups" {
FTP - [Second Component]
[First Component] --> FTP
}
cloud {
[Example 1]
}
database "MySql" {
folder "This is my folder" {
[Folder 3]
}
frame "Foo" {
[Frame 4]
}
}
[Another Component] --> [Example 1]
[Example 1] --> [Folder 3]
[Folder 3] --> [Frame 4]
@enduml
http://plantuml.com/state-diagram
@startuml
+[*] --> State1
+State1 --> [*]
+State1 : this is a string
+State1 : this is another string
+
+State1 -> State2
+State2 --> [*]
+
+scale 350 width
+[*] --> NotShooting
+
+state NotShooting {
+ [*] --> Idle
+ Idle --> Configuring : EvConfig
+ Configuring --> Idle : EvConfig
+}
+
+state Configuring {
+ [*] --> NewValueSelection
+ NewValueSelection --> NewValuePreview : EvNewValue
+ NewValuePreview --> NewValueSelection : EvNewValueRejected
+ NewValuePreview --> NewValueSelection : EvNewValueSaved
+
+ state NewValuePreview {
+ State1 -> State2
+ }
+}
+@enduml
+
@startuml
[] --> State1
State1 --> []
State1 : this is a string
State1 : this is another string
State1 -> State2
State2 --> [*]
scale 350 width
[*] --> NotShooting
state NotShooting {
[*] --> Idle
Idle --> Configuring : EvConfig
Configuring --> Idle : EvConfig
}
state Configuring {
[*] --> NewValueSelection
NewValueSelection --> NewValuePreview : EvNewValue
NewValuePreview --> NewValueSelection : EvNewValueRejected
NewValuePreview --> NewValueSelection : EvNewValueSaved
state NewValuePreview {
State1 -> State2
}
}
@enduml
@startuml
+nwdiag {
+ network dmz {
+ address = "210.x.x.x/24"
+
+ // set multiple addresses (using comma)
+ web01 [address = "210.x.x.1, 210.x.x.20"];
+ web02 [address = "210.x.x.2"];
+ }
+ network internal {
+ address = "172.x.x.x/24";
+
+ web01 [address = "172.x.x.1"];
+ web02 [address = "172.x.x.2"];
+ db01;
+ db02;
+ }
+}
+@enduml
+
@startuml
nwdiag {
network dmz {
address = "210.x.x.x/24"
// set multiple addresses (using comma)
+ web01 [address = "210.x.x.1, 210.x.x.20"];
+ web02 [address = "210.x.x.2"];
+
}
network internal {
address = "172.x.x.x/24";
web01 [address = "172.x.x.1"];
+ web02 [address = "172.x.x.2"];
+ db01;
+ db02;
+
}
}
@enduml
https://plantuml.com/gantt-diagram
@startuml
+@startgantt
+[Prototype design] lasts 15 days
+[Test prototype] lasts 10 days
+-- All example --
+[Task 1 (1 day)] lasts 1 day
+[T2 (5 days)] lasts 5 days
+[T3 (1 week)] lasts 1 week
+[T4 (1 week and 4 days)] lasts 1 week and 4 days
+[T5 (2 weeks)] lasts 2 weeks
+@endgantt
+@enduml
+
@startuml
@startgantt
[Prototype design] lasts 15 days
[Test prototype] lasts 10 days
-- All example --
[Task 1 (1 day)] lasts 1 day
[T2 (5 days)] lasts 5 days
[T3 (1 week)] lasts 1 week
[T4 (1 week and 4 days)] lasts 1 week and 4 days
[T5 (2 weeks)] lasts 2 weeks
@endgantt
@enduml
https://plantuml.com/mindmap-diagram
@startuml
+@startmindmap
+* Debian
+** Ubuntu
+*** Linux Mint
+*** Kubuntu
+*** Lubuntu
+*** KDE Neon
+** LMDE
+** SolydXK
+** SteamOS
+** Raspbian with a very long name
+*** <s>Raspmbc</s> => OSMC
+*** <s>Raspyfi</s> => Volumio
+@endmindmap
+@enduml
+
+
@startuml
@startmindmap
컨텐츠에 탭을 추가하여 상황에 따라 선택적으로 문서를 읽을 수 있도록 합니다.
상세 내용은 Markdown Enhance
의 Tabs, Code Tabs 를 확인해보세요.
::: tabs
+@tab title
+__markdown content__
+
+@tab javascript
+``` javascript
+() => {
+ console.log('Javascript code example')
+}
+```
+:::
+
다음과 같이 표기됩니다.
markdown content
() => {
+ console.log('Javascript code example')
+}
+
Code Tabs
는 코드 블록만을 표기하는 탭을 제공합니다.
::: code-tabs#shell
+
+@tab pnpm
+
+```bash
+pnpm add -D vuepress-plugin-md-enhance
+```
+
+@tab yarn
+
+```bash
+yarn add -D vuepress-plugin-md-enhance
+```
+
+@tab:active npm
+
+```bash
+npm i -D vuepress-plugin-md-enhance
+```
+
+:::
+
다음과 같이 표기됩니다.
pnpm add -D vuepress-plugin-md-enhance
+
yarn add -D vuepress-plugin-md-enhance
+
npm i -D vuepress-plugin-md-enhance
+
문서 작성시 팁과 주의사항을 표기하는 방법을 설명합니다.
공식 문서
::: tip
+This is a tip
+:::
+
+::: warning
+This is a warning
+:::
+
+::: danger
+This is a dangerous warning
+:::
+
+::: details
+This is a details block, which does not work in IE / Edge
+:::
+
다음과 같이 표기됩니다.
팁
This is a tip
경고
This is a warning
위험
This is a dangerous warning
This is a details block, which does not work in IE / Edge
타입 우측에 타이틀명을 추가하여 기본 값을 변경합니다.
::: danger STOP
+Danger zone, do not proceed
+Go to [here](https://vuepress.vuejs.org/guide/markdown.html#:~:text=markdown.toc%20option.-,%23,-Custom%20Containers)
+:::
+
+::: details Click me to view the code
+```js
+console.log('Hello, VuePress!')
+```
+:::
+
STOP
Danger zone, do not proceed
Go to here
console.log('Hello, VuePress!')
+
문서를 올바르게 작성하고 공유하기 위한 몇가지 사항을 안내합니다.
안내
문서 기여 시 문서 작성 가이드를 꼭 한번 확인해주세요.
docmoa를 활용할 수 있는 몇가지 가이드를 docmoa 활용 가이드 에서 설명합니다.
docmoa에 문서 기여를 위한 기본적인 몇가지 가이드를 설명합니다.
update : 2021. 12. 23.
CRI-O | Containerd CRI plugin | Docker Engine | gVisor CRI plugin | CRI-O Kata Containers | |
---|---|---|---|---|---|
sponsors | CNCF | CNCF | Docker Inc | Intel | |
started | 2016 | 2015 | Mar 2013 | 2015 | 2017 |
version | 1.23 | 1.19 | 20.10 | release-20211129.0 | 1.13 |
runtime | runc (default) | containerd managing runc | runc | runsc | kata-runtime |
kernel | shared | shared | shared | partially shared | isolated |
syscall filtering | no | no | no | yes | no |
kernel blobs | no | no | no | no | yes |
footprint | - | - | - | - | 30mb |
start time | <10ms | <10ms | <10ms | <10ms | <100ms |
io performance | host performance | host performance | host performance | slow | host performance |
network performance | host performance | host performance | host performance | slow (see comment) | close to host performance |
Docs | https://github.com/kubernetes-sigs/cri-o/ | https://github.com/containerd/cri | https://github.com/moby/moby | https://github.com/google/gvisor | https://github.com/kata-containers/runtime |
장점 | 경량의 쿠버네티스 전용 Docker 데몬이 필요하지 않음 OpenShift의 기본 컨테이너 런타임 아마도 최고의 컨테이너 기본 런타임 | 최신 Docker Engine과 함께 기본적으로 설치됨 Kubernetes는 ContainerD를 직접 사용할 수 있으며, Docker또한 동일한 호스트에서 직접 사용할 수도 있음 DockerD 데몬을 실행할 필요가 없음 | 방대한 수의 사용자가 테스트하고 반복 한 가장 성숙한 런타임 seccomp, SELinux 및 AppArmor를 사용하여 강화할 수 있음 가장 빠른 시작 시간 메모리 사용량이 가장 적음 | gcloud appengine에서 고객 간의 격리 계층으로 사용함 상태를 저장하지 않는 웹 앱에 적합 표준 컨테이너에 두 개의 보안 계층을 추가함 | 아마도 가장 안전한 옵션 보안에 대한 주요 절충안으로 오버헤드가 발생하는것은 그렇게 나쁘지 않은 것으로 보임 |
단점 | Docker Engine이 같고 있는 동일한 보안 이슈를 가지고 있음 보안정책을 별도로 관리해야 함 | This is slightly newer as it has been through a few iterations of being installed differently. | Kubernetes는 CRI 플러그인 아키텍처로 이동하고 있음 보안을 강화하고 관리하는것은 너무 복잡함 | 버전이 지정되지 않았으며 아직 Kubernetes에서 프로덕션에 사용해서는 안됨 많은 syscall을 만드는 응용 프로그램에는 적합하지 않음 400 개 Linux syscall이 모두 구현되어 일부 앱이 작동하지 않을 수 있음 (예 : postgres). | kata-runtime 자체는 v1이지만 이것이 Kubernetes 상에서 어떻게 준비 되어 있는지 확인이 필요 30MB 메모리 오버 헤드로 인한 비효율적 패킹 시작 시간 |
Private docker registry
Rancher Desktop
MacOS
Src : https://slack-archive.rancher.com/t/8508077/on-my-m1-mac-i-ve-started-getting-this-error-and-it-wont-go-#3e8d178c-aee8-46e6-b4cc-094c2339cbaa
$ docker run abc
+f631eb8a0078: Already exists
+d04f82e55126: Already exists
+0b1212f566e8: Already exists
+8e7d076cd7f0: Already exists
+62aa9a741295: Already exists
+f3e65750a6be: Extracting 22.02GB/22.02GB
+docker: failed to register layer: Error processing tar file(exit status 1): write /0000.dat: no space left on device.
+See 'docker run --help'.
+
위험
Check the Rancher desktop stopped
$ /Applications/Rancher\ Desktop.app/Contents/Resources/resources/darwin/lima/bin/qemu-img resize $HOME/Library/Application\ Support/rancher-desktop/lima/0/diffdisk +10G
+
+Image resized.
+
Private docker registry
Rancher Desktop
MacOS
Setupinsecure-registries
$ docker push 192.168.60.11:5000/example-python:1.0
+Error response from daemon: Get https://192.168.60.11:5000/v1/example-python: http: server gave HTTP response to HTTPS client
+
MAC-user-terminal $ LIMA_HOME="$HOME/Library/Application Support/rancher-desktop/lima" "/Applications/Rancher Desktop.app/Contents/Resources/resources/darwin/lima/bin/limactl" shell 0
+
+lima-rancher-desktop $ sudo vi /etc/conf.d/docker
+
...
+# DOCKER_OPTS="" to
+DOCKER_OPTS="--insecure-registry=192.168.60.11:5000"
+
lima-rancher-desktop $ sudo service docker restart
+ * Caching service dependencies .. [ ok ]
+ * Stopping Docker Daemon .. [ ok ]
+ * Starting Docker Daemon ... [ ok ]
+
+lima-rancher-desktop $ exit
+
$ docker push 192.168.60.11:5000/example-python:1.0
+The push refers to repository [192.168.60.11:5000/example-python]
+259faf3d45f5: Pushed
+d25777b53a01: Pushed
+b28e272ad893: Pushed
+517c4790d67b: Pushed
+40dc2004f48f: Pushed
+df63783f60fd: Pushed
+7e1c5e1a1242: Pushed
+b4440cc8e4ff: Pushed
+26a1b0864fd3: Pushed
+867d0767a47c: Pushed
+1.0: digest: sha256:dc8ebd032c0c1cd8fd991926eaea5a9d8f3cf66286c048e3bac6336969c524b5 size: 2414
+
직역하자면 너무많은 인증 실패로 인한 SSH 접속이 안된다.
는 메시지를 간혹 보게되는 경우가 있다.
$ ssh myserver
+Received disconnect from 192.168.0.43 port 22:2: Too many authentication failures
+Connection to 192.168.0.43 closed by remote host.
+Connection to 192.168.0.43 closed.
+
특히나 클라우드나 VM을 새로 프로비저닝 해서 사용하려고 할때 IP가 중복되어 재사용되어야 하는 경우에 주로 발생하는 걸로 추측된다.
위 메시지의 발생 원인은 이미 SSH로 접속하려고 하는 클라이언트 환경에 많은 SSH ID 정보가 저장되어있고, SSH Client를 실행할 때 ssh-agent로 이미 알고있는 모든 SSH 키와 다른 모든 키에 대해 접속을 시도하게 된다. 이때 SSH로 접속하고자 하는 원격 서버는 특정 ID 키로 맵핑되어있고, 기존의 키 정보와 맞지 않거나 동일한 대상에 대한 SSH ID 정보와 달라진 것이 원인으로 확인된다.
접속하고자 하는 Client 환경에서 SSH 키를 초기화 하는 방법
$ ssh-add -D
+
위와 같이 했을 때 Could not open a connection to your authentication agent.
와 같은 오류가 발생한다면 다음 방법으로 초기화 한다.
$ exec ssh-agent bash
+$ ssh-add -D
+All identities removed.
+
SSH 옵션으로 Public Key를 이용한 접속을 일시적으로 사용하지 않도록 하는 방법
$ ssh -p 22 -o PubkeyAuthentication=no username@myserver
+
~/.ssh/config
의 대상 호스트에 IdentitiesOnly=yes
를 추가하는 벙법
많은 ID를 제공 하더라도 ssh가 ssh_config 파일에 구성된 인증 ID 파일만 사용하도록 지정한다고 함
Host myserver
+IdentityFile ~/.ssh/key_rsa
+IdentitiesOnly yes
+Port 22
+
nomad alloc status d78d5b32
+ID = d78d5b32-00c3-5468-284a-8c201058c53a
+Eval ID = c6c9a1d9
+Name = 08_grafana.08_grafana[0]
+Node ID = e11b7729
+Node Name = slave1
+Job ID = 08_grafana
+Job Version = 0
+Client Status = running
+Client Description = Tasks are running
+Desired Status = run
+Desired Description = <none>
+Created = 18h42m ago
+Modified = 2h36m ago
+
+Allocation Addresses (mode = "bridge")
+Label Dynamic Address
+*http yes 10.0.0.161:25546
+*connect-proxy-grafana yes 10.0.0.161:29382 -> 29382
+
$ netstat -ntlp
+Active Internet connections (only servers)
+Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
+tcp 0 0 127.0.0.1:8502 0.0.0.0:* LISTEN 780/consul
+tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 813/sshd: /usr/sbin
+tcp 0 0 0.0.0.0:8888 0.0.0.0:* LISTEN 2354/haproxy
+tcp 0 0 127.0.0.1:8600 0.0.0.0:* LISTEN 780/consul
+tcp 0 0 10.0.0.161:24185 0.0.0.0:* LISTEN 22324/docker-proxy
+tcp 0 0 10.0.0.161:9090 0.0.0.0:* LISTEN 2496/docker-proxy
+tcp 0 0 172.30.1.112:8301 0.0.0.0:* LISTEN 780/consul
+tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN 1/init
+tcp 0 0 0.0.0.0:1936 0.0.0.0:* LISTEN 2354/haproxy
+tcp 0 0 127.0.0.1:8500 0.0.0.0:* LISTEN 780/consul
+tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN 33902/systemd-resol
+tcp6 0 0 :::22 :::* LISTEN 813/sshd: /usr/sbin
+tcp6 0 0 :::4646 :::* LISTEN 9827/nomad
+tcp6 0 0 :::111 :::* LISTEN 1/init
+tcp6 0 0 :::6100 :::* LISTEN 22827/java
+
$ docker ps | grep grafana
+0371b4c5f500 grafana/grafana "/run.sh" 50 minutes ago Up 50 minutes grafana-d78d5b32-00c3-5468-284a-8c201058c53a
+62a46e08d426 envoyproxy/envoy:v1.20.0 "/docker-entrypoint.…" 50 minutes ago Up 50 minutes connect-proxy-grafana-d78d5b32-00c3-5468-284a-8c201058c53a
+
+$ docker inspect -f '{{.State.Pid}}' 0371b4c5f500
+22741
+
+$ nsenter -t 22741 -n netstat -ntl
+Active Internet connections (only servers)
+Proto Recv-Q Send-Q Local Address Foreign Address State
+tcp 0 0 127.0.0.2:19001 0.0.0.0:* LISTEN
+tcp6 0 0 :::25546 :::* LISTEN
+
노드를 필터링하는 목적은 포드의 특정 요구 사항을 충족하지 않는 노드를 필터링하는 것입니다. 예를 들어 노드의 사용 가능한 리소스 (노드에서 이미 실행 된 모든 Pod의 리소스 요청 합계를 뺀 용량으로 측정)가 Pod의 필수 리소스보다 적은 경우 순위에서 노드를 고려해서는 안됩니다. 단계로 필터링됩니다. 현재 다음을 포함하여 서로 다른 필터링 정책을 구현하는 여러 "술어"가 있습니다.
finalScoreNodeA = (weight1 * priorityFunc1) + (weight2 * priorityFunc2)
+
+
모든 노드의 점수가 계산 된 후 점수가 가장 높은 노드가 포드의 호스트로 선택됩니다. 동일한 최고 점수를 가진 노드가 두 개 이상있는 경우 그중에서 임의의 노드가 선택됩니다.
현재 Kubernetes 스케줄러는 다음과 같은 실용적인 우선 순위 기능을 제공합니다.
# 먼저 설치하여 환경파일을 가져오고 원하는 버전을 설치한다.
+sudo apt-get install containerd -y
+
+sudo mkdir -p /etc/containerd
+
+containerd config default | sudo tee /etc/containerd/config.toml
+
+sudo systemctl stop containerd
+
+curl -LO https://github.com/containerd/containerd/releases/download/v1.4.4/containerd-1.4.4-linux-amd64.tar.gz
+
+tar xvf containerd-1.4.4-linux-amd64.tar.gz
+
+rm containerd-1.4.4-linux-amd64.tar.gz
+
+sudo cp bin/* /usr/bin/
+
+sudo systemctl start containerd
+
+rm -rf bin
+
+sudo systemctl status containerd --lines 1
+
+# k8s 설치시작
+curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add
+
+sudo apt-add-repository "deb http://apt.kubernetes.io/ kubernetes-xenial main"
+
+sudo apt-get install kubeadm kubelet kubectl -y
+
+sudo apt-mark hold kubeadm kubelet kubectl containerd
+
+#echo 'net.bridge.bridge-nf-call-iptables = 1' | sudo tee -a /etc/sysctl.conf
+
+SOURCE_FILE="/etc/sysctl.conf"
+LINE_INPUT="net.bridge.bridge-nf-call-iptables = 1"
+
+grep -qF "$LINE_INPUT" "$SOURCE_FILE" || echo "$LINE_INPUT" | sudo tee -a "$SOURCE_FILE"
+
+sudo echo '1' | sudo tee /proc/sys/net/ipv4/ip_forward
+
+cat /proc/sys/net/ipv4/ip_forward
+
+sudo sysctl --system
+
+sudo modprobe overlay
+sudo modprobe br_netfilter
+
+sudo swapoff -a
+
+sudo sed -ri '/\sswap\s/s/^#?/#/' /etc/fstab
+
+cat /etc/fstab
+
# k8s master server 설정
+sudo kubeadm config images pull
+
+IP_ADDR=`hostname -I | awk '{print $1}'`
+
+sudo kubeadm init --pod-network-cidr=10.244.0.0/16 --apiserver-advertise-address=${IP_ADDR}
+
+mkdir -p $HOME/.kube
+
+sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
+
+sudo chown $(id -u):$(id -g) $HOME/.kube/config
+
+# cni 설치(weave)
+kubectl apply -f "https://cloud.weave.works/k8s/net?k8s-version=$(kubectl version | base64 | tr -d '\n')"
+
+#kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
+
+sudo cp ./crictl.yaml /etc/crictl.yaml
+
+sudo crictl images
+
+watch -n 5 "kubectl get nodes"
+
+
vagrantfile
의 *.vm.network
부분의 ip
에 수정 필요 vagrantfile
구성시 해당 ip
설정 부분을 상단의 NETWORK_SUB
부분에 정의함vagrantfile
의 START_IP
를 활용하여 마스터 노드 및 워커 노드의 ip를 부여하는 방식으로 구성되었으나 변경 가능디렉토리/파일 구조
.
+├── `1.18`
+│ ├── files
+│ │ └── `pv.yaml`
+│ ├── scripts
+│ │ ├── `kube.sh`
+│ │ └── `pv.sh`
+│ └── `vagrantfile`
+├── 1.19
+<반복>
+
vagrantfile
에서 provision으로 호출vagrantfile
에서 provision으로 호출실행 후
├── 1.18
+│ ├── `.kube`
+│ ├── `.vagrant`
+│ ├── files
+│ │ └── pv.yaml
+│ ├── `join.sh`
+│ ├── `k8s-pv`
+│ │ ├── pv01
+│ │ ├── pv02
+│ │ └── pv03
+│ ├── scripts
+│ │ ├── kube.sh
+│ │ └── pv.sh
+│ └── vagrantfile
+├── 1.19
+<반복>
+
IMAGE_NAME = "bento/ubuntu-20.04"
+
+K8S_MINOR_VERSION = "21"
+NETWORK_SUB = "192.168.60."
+START_IP = 130
+POD_CIDR = "10.#{K8S_MINOR_VERSION}.0.0/16"
+
+cluster = {
+ "master" => { :cpus => 2, :mem => 2048 },
+ "node" => { :cpus => 1, :mem => 1024 }
+}
+
+NODE_COUNT = 1
+
+VM_GROUP_NAME = "k8s-1.#{K8S_MINOR_VERSION}"
+DOCKER_VER = "5:20.10.12~3-0~ubuntu-focal"
+KUBE_VER = "1.#{K8S_MINOR_VERSION}.8-00"
+
Variable name | value |
---|---|
IMAGE_NAME | vagrant에서 사용할 기본 이미지로 vagrant cloud 참조 |
K8S_MINOR_VERSION | K8s 설치의 마이너 버전 지정 |
NETWORK_SUB | Virtualbox network ip |
START_IP | 각 K8s 클러스터의 master에 할당되며 워커노드는 +1 씩 증가 |
POD_CIDR | kubeadm init 의 --pod-network-cidr 에 지정되는 CIDR |
cluster={} | 클러스터 리소스를 정의한 오브젝트 형태의 변수 |
NODE_COUNT | 워커 노드 개수 |
VM_GROUP_NAME | Virtualbox에 등록될 그룹 이름 |
DOCKER_VER | docker-ce 버전 |
KUBE_VER | K8s 관련 패키지 버전 |
Vagrant.configure("2") do |config|
+ config.vm.box = IMAGE_NAME
+
+ config.vm.define "master", primary: true do |master|
+ master.vm.box = IMAGE_NAME
+ master.vm.network "private_network", ip: "#{NETWORK_SUB}#{START_IP}"
+ master.vm.hostname = "master"
+ master.vm.provision "kube", type: "shell", privileged: true, path: "scripts/kube.sh", env: {
+ "docker_ver" => "#{DOCKER_VER}",
+ "k8s_ver" => "#{KUBE_VER}"
+ }
+ master.vm.provision "0", type: "shell", preserve_order: true, privileged: true, inline: <<-SHELL
+ OUTPUT_FILE=/vagrant/join.sh
+ rm -rf /vagrant/join.sh
+ rm -rf /vagrant/.kube
+ sudo kubeadm init --apiserver-advertise-address=#{NETWORK_SUB}#{START_IP} --pod-network-cidr=#{POD_CIDR}
+ sudo kubeadm token create --print-join-command > /vagrant/join.sh
+ chmod +x $OUTPUT_FILE
+ mkdir -p $HOME/.kube
+ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
+ sudo chown $(id -u):$(id -g) $HOME/.kube/config
+ cp -R $HOME/.kube /vagrant/.kube
+ #kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
+ kubectl apply -f https://docs.projectcalico.org/manifests/calico.yaml
+ kubectl completion bash >/etc/bash_completion.d/kubect
+ echo 'alias k=kubectl' >>~/.bashrc
+ SHELL
+
+ master.vm.provision "file", preserve_order: true, source: "files", destination: "/tmp"
+ master.vm.provision "3", type: "shell", preserve_order: true, privileged: true, path: "scripts/pv.sh"
+
+ master.vm.provider "virtualbox" do |v|
+ <생략>
+ end # end provider
+ end
+
+ (1..NODE_COUNT).each do |i|
+ config.vm.define "node-#{i}" do |node|
+ node.vm.box = IMAGE_NAME
+ node.vm.network "private_network", ip: "#{NETWORK_SUB}#{i + START_IP}"
+ node.vm.hostname = "node-#{i}"
+ <생략>
+
+ node.vm.provider "virtualbox" do |v|
+ <생략>
+ end # end provider
+ end
+ end
+end
+
master
와 node-#
로 구분됨master
의 경우 1개만을 생성하도록 지정*.vm.provision
을 통해 스크립트를 실행하거나 파일을 복사하여 프로비저닝master.vm.provision "0"
에서 kubeconfig
파일과 워커노드 조인을 위한 토큰 커맨드를 join.sh
파일로 생성NODE_COUNT
에서 지정된 개수에 따라 반복 수행master
프로비저닝시 생성된 join.sh
를 이용하여 클러스터에 조인cli doc : https://www.vagrantup.com/docs/cli
vm-name
: config.vm.define
에 선언된 아이디에 해당하는 vm만 기동 vm-name
: config.vm.define
에 선언된 아이디에 해당하는 vm만 정지 vm-name
: config.vm.define
에 선언된 아이디에 해당하는 vm에 ssh 접속 master
노드 프로비저닝 과정에서 .kube/config
를 생성하므로, 해당 kubeconfig를 사용하여 호스트 환경에서 kubectl
사용 가능
~/vagrant-k8s/1.23> kubectl --kubeconfig=./.kube/config get nodes
+NAME STATUS ROLES AGE VERSION
+master Ready control-plane,master 68m v1.23.1
+node-1 Ready <none> 63m v1.23.1
+node-2 Ready <none> 59m v1.23.1
+node-3 Ready <none> 54m v1.23.1
+
#apt-cache policy <packagename>
+apt-cache policy kubelet | grep 1.2
+ Candidate: 1.23.1-00
+ 1.23.1-00 500
+ 1.23.0-00 500
+ 1.22.5-00 500
+ 1.22.4-00 500
+ 1.22.3-00 500
+ 1.22.2-00 500
+ 1.22.1-00 500
+ 1.22.0-00 500
+ 1.21.8-00 500
+ 1.21.7-00 500
+ ...
+
💡 본 글은 PKOS(Production Kubernetes Online Study) 2기 스터디의 일부로 작성된 내용입니다.
실제 Production Kubernetes 환경에서 활용 가능한 다양한 정보를 전달하기 위한 시리즈로 작성 예정입니다.
본 스터디는 AWS 환경에서 Kops(Kubernetes Operations)를 활용한 실습으로 진행할 예정입니다.
📌 참고 : 필자는 개인적인 이유로 Route 53 계정과, kOps 클러스터 운영 계정을 나눠서 진행합니다.
하나의 계정에서 실습을 진행할 경우에는 사전 환경구성이 다를 수 있는 점 참고 부탁드립니다.
AWS의 DNS 웹 서비스인 Route 53을 통해 도메인을 구입합니다.
필자는 hyungwook.link
도메인을 구입하였으며, 초기 구입 후 상태: 도메인 등록 진행 중
인 화면을 확인할 수 있습니다,
구입 시 등록했던 이메일 계정으로 발송된 verify 메일 링크를 클릭하여 활성화 합니다.
일정시간이 지나면 정상적으로 도메인이 활성화 되된 것을 확인할 수 있습니다.
# 자신의 도메인에 NS 타입 조회
+# dig ns <구입한 자신의 도메인> +short
+dig ns hyungwook.link +short
+ns-939.awsdns-53.net.
+ns-1575.awsdns-04.co.uk.
+ns-233.awsdns-29.com.
+ns-1466.awsdns-55.org.
+
필자는 서두에서 언급한 것 처럼 Route 53 구매한 계정과, kOps 클러스터를 생성할 계정이 다르므로 다음과 같은 과정을 추가적으로 수행하였습니다.
pkos.hyungwook.link
레코드를 생성📌 참고 : How to manage Route53 hosted zones in a multi-account environment
이제 실습에서 사용할 도메인 준비가 완료되었으므로, Kops 클러스터 생성을 위한 준비 단계로 넘어갑니다.
# IAM User 자격 구성 : 실습 편리를 위해 administrator 권한을 가진 IAM User 의 자격 증명 입력
+aws configure
+
# k8s 설정 파일이 저장될 버킷 생성
+## aws s3 mb s3://버킷<유일한 이름> --region <S3 배포될 AWS 리전>
+aws s3 mb s3://버킷<유일한 이름> --region $REGION
+aws s3 ls
+
+## 예시)
+aws s3 mb s3://hyungwook-k8s-s3 --region ap-northeast-2
+
# 변수설정
+export AWS_PAGER=""
+export REGION=ap-northeast-2
+export KOPS_CLUSTER_NAME=pkos.hyungwook.link
+export KOPS_STATE_STORE=s3://hyungwook-k8s-s3
+echo 'export AWS_PAGER=""' >>~/.bashrc
+echo 'export REGION=ap-northeast-2' >>~/.bashrc
+echo 'export KOPS_CLUSTER_NAME=pkos.hyungwook.link' >>~/.bashrc
+echo 'export KOPS_STATE_STORE=s3://hyungwook-k8s-s3' >>~/.bashrc
+
+# kops 설정 파일 생성(s3) 및 k8s 클러스터 배포 : 6분 정도 소요
+## CNI는 aws vpc cni 사용, 마스터 노드 1대(t3.medium), 워커 노드 2대(t3.medium), 파드 사용 네트워크 대역 지정(172.30.0.0/16)
+## --container-runtime containerd --kubernetes-version 1.24.0 ~ 1.25.6
+kops create cluster --zones="$REGION"a,"$REGION"c --networking amazonvpc --cloud aws \
+--master-size t3.medium --node-size t3.medium --node-count=2 --network-cidr 172.30.0.0/16 \
+--ssh-public-key ~/.ssh/id_rsa.pub --name=$KOPS_CLUSTER_NAME --kubernetes-version "1.24.10" --dry-run -o yaml > mykops.yaml
+
+kops create cluster --zones="$REGION"a,"$REGION"c --networking amazonvpc --cloud aws \
+--master-size t3.medium --node-size t3.medium --node-count=2 --network-cidr 172.30.0.0/16 \
+--ssh-public-key ~/.ssh/id_rsa.pub --name=$KOPS_CLUSTER_NAME --kubernetes-version "1.24.10" -y
+
A 레코드값이 자동으로 추가된 모습을 확인할 수 있습니다. 하지만 실제 api 서버와 내부 controller의 IP 주소가 등록되지 않았기 때문에, 실제 클러스터가 정상적으로 구성된 이후에는 자동으로 A 레코드가 업데이트 됩니다.
aws route53
aws route53 list-resource-record-sets --hosted-zone-id "${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'A'].Name" | jq
+
+[
+ "api.pkos.hyungwook.link.",
+ "api.internal.pkos.hyungwook.link.",
+ "kops-controller.internal.pkos.hyungwook.link."
+]
+
이때, kops validate
명령으로 확인하면 아직까지 api.pkos.hyungwook.link
가 relov 되지 않는 것을 확인할 수 있습니다.
kops validate cluster --wait 10m
+Validating cluster pkos.hyungwook.link
+
+W0305 22:38:08.780600 4256 validate_cluster.go:184] (will retry): unexpected error during validation: unable to resolve Kubernetes cluster API URL dns: lookup api.pkos.hyungwook.link: no such host
+W0305 22:38:18.788067 4256 validate_cluster.go:184] (will retry): unexpected error during validation: unable to resolve Kubernetes cluster API URL dns: lookup api.pkos.hyungwook.link: no such host
+
어느정도 시간이 지난 후 정상적으로 A 레코드 값이 변경된 것을 확인할 수 있습니다.
# A 레코드 및 값 반복조회
+while true; do aws route53 list-resource-record-sets --hosted-zone-id "${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'A']" | jq ; date ; echo ; sleep 1; done
+[
+ {
+ "Name": "api.pkos.hyungwook.link.",
+ "Type": "A",
+ "TTL": 60,
+ "ResourceRecords": [
+ {
+ "Value": "43.201.33.161"
+ }
+ ]
+ },
+ {
+ "Name": "api.internal.pkos.hyungwook.link.",
+ "Type": "A",
+ "TTL": 60,
+ "ResourceRecords": [
+ {
+ "Value": "172.30.37.41"
+ }
+ ]
+ },
+ {
+ "Name": "kops-controller.internal.pkos.hyungwook.link.",
+ "Type": "A",
+ "TTL": 60,
+ "ResourceRecords": [
+ {
+ "Value": "172.30.37.41"
+ }
+ ]
+ }
+]
+2023년 3월 5일 일요일 22시 41분 46초 KST
+
이제 정상적으로 A 레코드가 등록된 것을 확인할 수 있으며 설치가 자동으로 진행됩니다.
kops validate cluster
명령(생성확인)실제 kops 클러스터가 정상적으로 배포된 것을 확인할 수 있습니다.
kops validate cluster
+Validating cluster pkos.hyungwook.link
+
+INSTANCE GROUPS
+NAME ROLE MACHINETYPE MIN MAX SUBNETS
+master-ap-northeast-2a Master t3.medium 1 1 ap-northeast-2a
+nodes-ap-northeast-2a Node t3.medium 1 1 ap-northeast-2a
+nodes-ap-northeast-2c Node t3.medium 1 1 ap-northeast-2c
+
+NODE STATUS
+NAME ROLE READY
+i-089062ff9f50789ee node True
+i-096a645be0dd932b6 node True
+i-0dce8997b4633b806 master True
+
+Your cluster pkos.hyungwook.link is ready
+
📌 참고 : Kops 클러스터
kubeconfig
파일 업데이트 명령
# 권한이 없을 경우
+kubectl get nodes -o wide
+error: You must be logged in to the server (Unauthorized)
+
+# kubeconfig 업데이트
+kops export kubeconfig --name pkos.hyungwook.link --admin
+
# 수퍼마리오 디플로이먼트 배포
+curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/1/mario.yaml
+kubectl apply -f mario.yaml
+cat mario.yaml | yh
+deployment.apps/mario created
+service/mario created
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: mario
+ labels:
+ app: mario
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: mario
+ template:
+ metadata:
+ labels:
+ app: mario
+ spec:
+ containers:
+ - name: mario
+ image: pengbai/docker-supermario
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: mario
+spec:
+ selector:
+ app: mario
+ ports:
+ - port: 80
+ protocol: TCP
+ targetPort: 8080
+ type: LoadBalancer
+
# 배포 확인 : CLB 배포 확인 >> 5분 이상 소요
+kubectl get deploy,svc,ep mario
+watch kubectl get svc mario
+
+# Watch 명령 실행 후 <pending>
+Every 2.0s: kubectl get svc mario hyungwook-Q9W5QX7FGY: Sat Mar 11 21:50:41 2023
+NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
+mario LoadBalancer 100.67.138.178 <pending> 80:30624/TCP 92s
+
+# External-IP 할당
+kubectl get svc mario
+NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
+mario LoadBalancer 100.67.138.178 a643cc3e6e2c54ed8989c95d0481f48c-113657418.ap-northeast-2.elb.amazonaws.com 80:30624/TCP 3m7s
+
# 마리오 게임 접속 : CLB 주소로 웹 접속
+kubectl get svc mario -o jsonpath="{.status.loadBalancer.ingress[0].hostname}" | awk '{ print "Maria URL = http://"$1 }'
+
+# 결과 값
+Maria URL = http://a643cc3e6e2c54ed8989c95d0481f48c-113657418.ap-northeast-2.elb.amazonaws.com
+
External DNS은 K8s Service / Ingress 생성 시 도메인을 설정하면 자동으로 AWS Route53의 A 레코드(TXT 레코드)에 자동 생성/삭제를 제공합니다.
# 정책 생성 -> 마스터/워커노드에 정책 연결
+curl -s -O https://s3.ap-northeast-2.amazonaws.com/cloudformation.cloudneta.net/AKOS/externaldns/externaldns-aws-r53-policy.json
+aws iam create-policy --policy-name AllowExternalDNSUpdates --policy-document file://externaldns-aws-r53-policy.json
+
+# ACCOUNT_ID 변수 지정
+export ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)
+
+# EC2 instance profiles 에 IAM Policy 추가(attach)
+aws iam attach-role-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AllowExternalDNSUpdates --role-name masters.$KOPS_CLUSTER_NAME
+aws iam attach-role-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AllowExternalDNSUpdates --role-name nodes.$KOPS_CLUSTER_NAME
+
+# 설치
+kops edit cluster
+--------------------------
+spec:
+ certManager: # 없어도됨!
+ enabled: true # 없어도됨!
+ externalDns:
+ provider: external-dns
+--------------------------
+
+# 업데이트 적용
+kops update cluster --yes && echo && sleep 3 && kops rolling-update cluster
+
+# externalDns 컨트롤러 파드 확인
+kubectl get pod -n kube-system -l k8s-app=external-dns
+NAME READY STATUS RESTARTS AGE
+external-dns-5bc8fcf8-7vznp 1/1 Running 0 14s
+
# CLB에 ExternanDNS 로 도메인 연결
+kubectl annotate service mario "external-dns.alpha.kubernetes.io/hostname=mario.$KOPS_CLUSTER_NAME"
+
# external-dns 등록로그 확인
+kubectl logs -n kube-system -l k8s-app=external-dns
+
+time="2023-03-11T14:54:51Z" level=info msg="Applying provider record filter for domains: [pkos.hyungwook.link. .pkos.hyungwook.link.]"
+time="2023-03-11T14:54:51Z" level=info msg="All records are already up to date"
+...(생략)
+
+# 확인
+dig +short mario.$KOPS_CLUSTER_NAME
+
+# 웹 접속 주소 확인 및 접속
+echo -e "Maria Game URL = http://mario.$KOPS_CLUSTER_NAME"
+
+# 도메인 체크
+echo -e "My Domain Checker = https://www.whatsmydns.net/#A/mario.$KOPS_CLUSTER_NAME"
+
kubectl delete deploy,svc mario
+
kops delete cluster --yes
+
본 편에서는 Kops 클러스터를 구성방안 및 External DNS를 연동한 외부 서비스 노출에 대한 방법을 살펴보았습니다.
다음편에서는 네트워크 및 스토리지에 대한 활용방안을 살펴보겠습니다.
지난 1주차 스터디에이어 2주차 스터디를 진행하였습니다. 이번 스터디에서는 "쿠버네티스 네트워크" 및 "쿠버네티스 스토리지"를 중심으로 학습하였습니다.
참고 :
원활한 실습을 위해 인스턴스 타입을 변경한 후 진행합니다.
kops get ig
+NAME ROLE MACHINETYPE MIN MAX ZONES
+master-ap-northeast-2a Master t3.medium 1 1 ap-northeast-2a
+nodes-ap-northeast-2a Node t3.medium 1 1 ap-northeast-2a
+nodes-ap-northeast-2c Node t3.medium 1 1 ap-northeast-2c
+
kops edit ig master-ap-northeast-2a
+
+# 예제화면
+apiVersion: kops.k8s.io/v1alpha2
+kind: InstanceGroup
+metadata:
+ creationTimestamp: "2023-03-05T13:37:26Z"
+ labels:
+ kops.k8s.io/cluster: pkos.hyungwook.link
+ name: master-ap-northeast-2a
+spec:
+ image: 099720109477/ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-20230112
+ instanceMetadata:
+ httpPutResponseHopLimit: 3
+ httpTokens: required
+ machineType: t3.medium #기존 t3.medium에서 c5d.large로 변경
+ maxSize: 1
+ minSize: 1
+ role: Master
+ subnets:
+ - ap-northeast-2a
+
kops get ig
+NAME ROLE MACHINETYPE MIN MAX ZONES
+master-ap-northeast-2a Master c5d.large 1 1 ap-northeast-2a
+nodes-ap-northeast-2a Node c5d.large 1 1 ap-northeast-2a
+nodes-ap-northeast-2c Node c5d.large 1 1 ap-northeast-2c
+
kops update cluster --name pkos.hyungwook.link --yes
+
+kops rolling-update cluster --yes
+
externalTrafficPolicy
: ClusterIP ⇒ 2번 분산 및 SNAT으로 Client IP 확인 불가능 ← LoadBalancer
타입 (기본 모드) 동작externalTrafficPolicy
: Local ⇒ 1번 분산 및 ClientIP 유지, 워커 노드의 iptables 사용함반드시 AWS LoadBalancer 컨트롤러 파드 및 정책 설정이 필요함!
Proxy Protocol v2 비활성화
⇒ NLB에서 바로 파드로 인입, 단 ClientIP가 NLB로 SNAT 되어 Client IP 확인 불가능Proxy Protocol v2 활성화
⇒ NLB에서 바로 파드로 인입 및 ClientIP 확인 가능(→ 단 PPv2 를 애플리케이션이 인지할 수 있게 설정 필요)# 작업용 EC2 - 디플로이먼트 & 서비스 생성
+cat ~/pkos/2/echo-service-nlb.yaml | yh
+kubectl apply -f ~/pkos/2/echo-service-nlb.yaml
+
+# 확인
+kubectl get deploy,pod
+kubectl get svc,ep,ingressclassparams,targetgroupbindings
+
+NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
+service/kubernetes ClusterIP 100.64.0.1 <none> 443/TCP 7d
+service/svc-nlb-ip-type LoadBalancer 100.64.191.200 k8s-default-svcnlbip-bfcad9371a-250be02681485d95.elb.ap-northeast-2.amazonaws.com 80:31206/TCP 97s
+
+NAME ENDPOINTS AGE
+endpoints/kubernetes 172.30.37.41:443 7d
+endpoints/svc-nlb-ip-type 172.30.55.31:8080,172.30.71.86:8080 97s
+
+NAME GROUP-NAME SCHEME IP-ADDRESS-TYPE AGE
+ingressclassparams.elbv2.k8s.aws/alb 122m
+
+NAME SERVICE-NAME SERVICE-PORT TARGET-TYPE AGE
+targetgroupbinding.elbv2.k8s.aws/k8s-default-svcnlbip-c54bafee9a svc-nlb-ip-type 80 ip 95s
+
+kubectl get targetgroupbindings -o json | jq
+
k get pods -o wide
+NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
+deploy-echo-(생략) 1/1 Running 0 7m50s 172.30.55.31 i-089062ff9f50789ee <none> <none>
+deploy-echo-(생략) 1/1 Running 0 7m50s 172.30.71.86 i-096a645be0dd932b6 <none> <none>
+
사전 준비 :
공인도메인 소유, AWS Route53 도메인등록 상태, NLB 가 위치한 리전(서울)의 인증서 요청/발급 완료상태, ExternalDNS 준비완료 상태
# 사용 리전의 인증서 ARN 확인
+aws acm list-certificates
+aws acm list-certificates --max-items 10
+aws acm list-certificates --query 'CertificateSummaryList[].CertificateArn[]' --output text
+CERT_ARN=`aws acm list-certificates --query 'CertificateSummaryList[].CertificateArn[]' --output text`
+echo $CERT_ARN
+
# 자신의 도메인 변수 지정
+MyDomain=<자신의 도메인>
+MyDomain=websrv.$KOPS_CLUSTER_NAME
+
cat <<EOF | kubectl create -f -
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: deploy-echo
+spec:
+ replicas: 2
+ selector:
+ matchLabels:
+ app: deploy-websrv
+ template:
+ metadata:
+ labels:
+ app: deploy-websrv
+ spec:
+ terminationGracePeriodSeconds: 0
+ containers:
+ - name: akos-websrv
+ image: k8s.gcr.io/echoserver:1.5
+ ports:
+ - containerPort: 8080
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: svc-nlb-ip-type
+ annotations:
+ external-dns.alpha.kubernetes.io/hostname: "${MyDomain}"
+ service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
+ service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
+ service.beta.kubernetes.io/aws-load-balancer-healthcheck-port: "8080"
+ service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
+ service.beta.kubernetes.io/aws-load-balancer-ssl-ports: "https"
+ service.beta.kubernetes.io/aws-load-balancer-ssl-cert: ${CERT_ARN}
+ service.beta.kubernetes.io/aws-load-balancer-backend-protocol: "http"
+spec:
+ ports:
+ - port: 80
+ targetPort: 8080
+ protocol: TCP
+ name: http
+ - port: 443
+ targetPort: 8080
+ protocol: TCP
+ name: https
+ type: LoadBalancer
+ loadBalancerClass: service.k8s.aws/nlb
+ selector:
+ app: deploy-websrv
+EOF
+
kubectl describe svc svc-nlb-ip-type | grep Annotations: -A8
+
+Annotations: external-dns.alpha.kubernetes.io/hostname: websrv.pkos.hyungwook.link
+ service.beta.kubernetes.io/aws-load-balancer-backend-protocol: http
+ service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: true
+ service.beta.kubernetes.io/aws-load-balancer-healthcheck-port: 8080
+ service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
+ service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
+ service.beta.kubernetes.io/aws-load-balancer-ssl-cert:
+ arn:aws:acm:ap-northeast-2:856117747411:certificate/208e809e-9ebf-4bb5-92c2-795868429e88
+ service.beta.kubernetes.io/aws-load-balancer-ssl-ports: https
+
insecure
옵션 없이 정상적으로 curl 응답 하는 것을 확인할 수 있습니다.curl -s http://websrv.pkos.hyungwook.link | grep Hostname
+Hostname: deploy-echo-5c4856dfd6-267pf
+
+curl -s https://websrv.pkos.hyungwook.link | grep Hostname
+Hostname: deploy-echo-5c4856dfd6-k9277
+
클러스터 내부의 서비스(ClusterIP, NodePort, Loadbalancer)를 외부로 노출(HTTP/HTTPS) - Web Proxy 역할
# EC2 instance profiles 에 IAM Policy 추가(attach)
+aws iam attach-role-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy --role-name masters.$KOPS_CLUSTER_NAME
+aws iam attach-role-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy --role-name nodes.$KOPS_CLUSTER_NAME
+
+# EC2 instance profiles 에 IAM Policy 추가(attach)
+aws iam attach-role-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy --role-name masters.$KOPS_CLUSTER_NAME
+aws iam attach-role-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy --role-name nodes.$KOPS_CLUSTER_NAME
+
# kOps 클러스터 편집 : 아래 내용 추가
+kops edit cluster
+-----
+spec:
+ certManager:
+ enabled: true
+ awsLoadBalancerController:
+ enabled: true
+ externalDns:
+ provider: external-dns
+
# 업데이트 적용
+kops update cluster --yes && echo && sleep 3 && kops rolling-update cluster
+
# 게임 파드와 Service, Ingress 배포
+kubectl apply -f ~/pkos/3/ingress1.yaml
+
kubectl get targetgroupbindings -n game-2048
+NAME SERVICE-NAME SERVICE-PORT TARGET-TYPE AGE
+k8s-game2048-service2-e48050abac service-2048 80 ip 87s
+
kubectl describe ingress -n game-2048 ingress-2048
+
+Name: ingress-2048
+Labels: <none>
+Namespace: game-2048
+Address: k8s-game2048-ingress2-fdfe8009a9-1424012699.ap-northeast-2.elb.amazonaws.com
+Ingress Class: alb
+Default backend: <default>
+Rules:
+ Host Path Backends
+ ---- ---- --------
+ *
+ / service-2048:80 (172.30.44.132:80,172.30.65.100:80)
+Annotations: alb.ingress.kubernetes.io/scheme: internet-facing
+ alb.ingress.kubernetes.io/target-type: ip
+Events:
+ Type Reason Age From Message
+ ---- ------ ---- ---- -------
+ Normal SuccessfullyReconciled 8m56s ingress Successfully reconciled
+
# 게임 접속 : ALB 주소로 웹 접속
+kubectl get ingress -n game-2048 ingress-2048 -o jsonpath="{.status.loadBalancer.ingress[0].hostname}" | awk '{ print "Game URL = http://"$1 }'
+Game URL = http://k8s-game2048-ingress2-fdfe8009a9-1424012699.ap-northeast-2.elb.amazonaws.com
+
kubectl delete ingress ingress-2048 -n game-2048
+kubectl delete svc service-2048 -n game-2048 && kubectl delete deploy deployment-2048 -n game-2048 && kubectl delete ns game-2048
+
c5d.large
의 EC2 인스턴스 스토어(임시 블록 스토리지) 설정 작업 - 링크 , NVMe SSD - 링크
# 인스턴스 스토어 볼륨이 있는 c5 모든 타입의 스토리지 크기
+aws ec2 describe-instance-types \
+ --filters "Name=instance-type,Values=c5*" "Name=instance-storage-supported,Values=true" \
+ --query "InstanceTypes[].[InstanceType, InstanceStorageInfo.TotalSizeInGB]" \
+ --output table
+--------------------------
+| DescribeInstanceTypes |
++---------------+--------+
+| c5d.large | 50 |
+| c5d.12xlarge | 1800 |
+...
+
+# 워커 노드 Public IP 확인
+aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value}" --filters Name=instance-state-name,Values=running --output table
+
+-------------------------------------------------------------------------
+| DescribeInstances |
++------------------------------------------------------+----------------+
+| InstanceName | PublicIPAdd |
++------------------------------------------------------+----------------+
+| nodes-ap-northeast-2c.pkos.hyungwook.link | 13.209.75.228 |
+| master-ap-northeast-2a.masters.pkos.hyungwook.link | 3.38.117.78 |
+| nodes-ap-northeast-2a.pkos.hyungwook.link | 52.79.61.228 |
++------------------------------------------------------+----------------+
+
+# 워커 노드 Public IP 변수 지정
+W1PIP=<워커 노드 1 Public IP>
+W2PIP=<워커 노드 2 Public IP>
+W1PIP=13.209.75.228
+W2PIP=52.79.61.228
+echo "export W1PIP=$W1PIP" >> /etc/profile
+echo "export W2PIP=$W2PIP" >> /etc/profile
+
ssh -i ~/.ssh/id_rsa ubuntu@$W1PIP sudo apt install -y nvme-cli
+ssh -i ~/.ssh/id_rsa ubuntu@$W2PIP sudo apt install -y nvme-cli
+ssh -i ~/.ssh/id_rsa ubuntu@$W1PIP sudo nvme list
+ssh -i ~/.ssh/id_rsa ubuntu@$W2PIP sudo nvme list
+
# 워커 노드 스토리지 확인 : NVMe SSD 인스턴스 스토어 볼륨 확인
+ssh -i ~/.ssh/id_rsa ubuntu@$W1PIP lsblk -e 7
+ssh -i ~/.ssh/id_rsa ubuntu@$W2PIP lsblk -e 7
+NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
+nvme1n1 259:0 0 46.6G 0 disk
+nvme0n1 259:1 0 128G 0 disk
+├─nvme0n1p1 259:2 0 127.9G 0 part /
+├─nvme0n1p14 259:3 0 4M 0 part
+└─nvme0n1p15 259:4 0 106M 0 part /boot/efi
+
+ssh -i ~/.ssh/id_rsa ubuntu@$W1PIP df -hT -t ext4
+ssh -i ~/.ssh/id_rsa ubuntu@$W2PIP df -hT -t ext4
+Filesystem Type Size Used Avail Use% Mounted on
+/dev/root ext4 124G 4.2G 120G 4% /
+
+ssh -i ~/.ssh/id_rsa ubuntu@$W1PIP lspci | grep Non-Volatile
+00:04.0 Non-Volatile memory controller: Amazon.com, Inc. Device 8061
+00:1f.0 Non-Volatile memory controller: Amazon.com, Inc. NVMe SSD Controller
+
+# 파일시스템 생성
+ssh -i ~/.ssh/id_rsa ubuntu@$W1PIP sudo mkfs -t xfs /dev/nvme1n1
+ssh -i ~/.ssh/id_rsa ubuntu@$W2PIP sudo mkfs -t xfs /dev/nvme1n1
+
+# /data 디렉토리 생성
+ssh -i ~/.ssh/id_rsa ubuntu@$W1PIP sudo mkdir /data
+ssh -i ~/.ssh/id_rsa ubuntu@$W2PIP sudo mkdir /data
+
+# /data 마운트
+ssh -i ~/.ssh/id_rsa ubuntu@$W1PIP sudo mount /dev/nvme1n1 /data
+ssh -i ~/.ssh/id_rsa ubuntu@$W2PIP sudo mount /dev/nvme1n1 /data
+
+# 파일시스템 포맷 및 마운트 확인
+ssh -i ~/.ssh/id_rsa ubuntu@$W1PIP df -hT -t ext4 -t xfs
+ssh -i ~/.ssh/id_rsa ubuntu@$W2PIP df -hT -t ext4 -t xfs
+Filesystem Type Size Used Avail Use% Mounted on
+/dev/root ext4 124G 4.2G 120G 4% /
+/dev/nvme1n1 xfs 47G 365M 47G 1% /data
+
# 마스터노드의 이름 확인해두기
+kubectl get node | grep control-plane | awk '{print $1}'
+i-066cdb714fc6545c0
+
+# 배포 : vim 직접 편집할것
+curl -s -O https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.23/deploy/local-path-storage.yaml
+vim local-path-storage.yaml
+----------------------------
+# 아래 빨간 부분은 추가 및 수정
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: local-path-provisioner
+ namespace: local-path-storage
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: local-path-provisioner
+ template:
+ metadata:
+ labels:
+ app: local-path-provisioner
+ spec:
+ nodeSelector:
+ kubernetes.io/hostname: "<각자 자신의 마스터 노드 이름 입력>"
+ tolerations:
+ - effect: NoSchedule
+ key: node-role.kubernetes.io/control-plane
+ operator: Exists
+...
+kind: ConfigMap
+apiVersion: v1
+metadata:
+ name: local-path-config
+ namespace: local-path-storage
+data:
+ config.json: |-
+ {
+ "nodePathMap":[
+ {
+ "node":"DEFAULT_PATH_FOR_NON_LISTED_NODES",
+ "paths":["/data/local-path"]
+ }
+ ]
+ }
+----------------------------
+
+# 배포
+kubectl apply -f local-path-storage.yaml
+
+# 확인 : 마스터노드에 배포됨
+kubectl get-all -n local-path-storage
+NAME NAMESPACE AGE
+configmap/kube-root-ca.crt local-path-storage 12s
+configmap/local-path-config local-path-storage 12s
+pod/local-path-provisioner-6bff65dcd8-vgwfk local-path-storage 12s
+serviceaccount/default local-path-storage 12s
+serviceaccount/local-path-provisioner-service-account local-path-storage 12s
+deployment.apps/local-path-provisioner local-path-storage 12s
+replicaset.apps/local-path-provisioner-6bff65dcd8 local-path-storage 12s
+
+kubectl get pod -n local-path-storage -owide
+NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
+local-path-provisioner-6bff65dcd8-vgwfk 1/1 Running 0 17s 172.30.63.103 i-072786762169226a7 <none> <none>
+
+kubectl describe cm -n local-path-storage local-path-config
+
+kubectl get sc local-path
+NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
+local-path rancher.io/local-path Delete WaitForFirstConsumer false 34s
+
# PVC 생성
+kubectl apply -f ~/pkos/3/localpath1.yaml
+
+# PVC 확인 : 아직 Pod Boud가 되지 않았으므로 Pending
+kubectl describe pvc
+kubectl get pvc
+
+NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
+localpath-claim Pending local-path 58s
+
+# 파드 생성
+kubectl apply -f ~/pkos/3/localpath2.yaml
+
+# 파드확인 : 정상적으로 Bound된 것으로 확인
+kubectl get pod,pv,pvc
+
+NAME READY STATUS RESTARTS AGE
+pod/app-localpath 1/1 Running 0 56s
+
+NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
+persistentvolume/pvc-37743f20-e30d-491c-b11c-5e5b7d33a476 1Gi RWO Delete Bound default/localpath-claim local-path 49s
+
+NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
+persistentvolumeclaim/localpath-claim Bound pvc-37743f20-e30d-491c-b11c-5e5b7d33a476 1Gi RWO local-path 3m57s
+
# 파드 데이터 확인 : app-localpath 파드에서 데이터 쌓이는 것 확인
+kubectl exec -it app-localpath -- tail -f /data/out.txt
+Sun Jan 29 05:13:45 UTC 2023
+
+ssh -i ~/.ssh/id_rsa ubuntu@$W2PIP tree /data
+/data
+0 directories, 0 files
+
+ssh -i ~/.ssh/id_rsa ubuntu@$W1PIP tree /data
+/data
+└── local-path
+ └── pvc-37743f20-e30d-491c-b11c-5e5b7d33a476_default_localpath-claim
+ └── out.txt
+
+2 directories, 1 file
+
+# 노드 데이터 확인 : app-localpath 파드가 배포된 노드에 접속 후 데이터 쌓이는 것 확인
+ssh -i ~/.ssh/id_rsa ubuntu@$W2PIP tail -f /data/local-path/pvc-ce742b90-755a-4b52-9693-595cbf55dfb0_default_localpath-claim/out.txt
+Sun Jan 29 05:13:45 UTC 2023
+...
+
LocalPath 성능측정은 추후 별도 정리 후 업로드 예정
Volume (ebs-csi-controller) : EBS CSI driver 동작 : 볼륨 생성 및 파드에 볼륨 연결 - 링크
# EBS Driver 확인 Kops 설치 시 기본 배포
+kubectl get pod -n kube-system -l app.kubernetes.io/instance=aws-ebs-csi-driver
+
+NAME READY STATUS RESTARTS AGE
+ebs-csi-controller-6d8fd64c78-q5qfn 5/5 Running 0 5d23h
+ebs-csi-node-9cfss 3/3 Running 0 5d23h
+ebs-csi-node-crhbx 3/3 Running 0 5d23h
+ebs-csi-node-zbjgj 3/3 Running 0 5d23h
+
+# 스토리지 클래스 확인
+kubectl get sc kops-csi-1-21 kops-ssd-1-17
+
+kubectl describe sc kops-csi-1-21 | grep Parameters
+Parameters: encrypted=true,type=gp3
+kubectl describe sc kops-ssd-1-17 | grep Parameters
+Parameters: encrypted=true,type=gp2
+
+
# PVC 생성
+kubectl apply -f ~/pkos/3/awsebs-pvc.yaml
+
+# 파드 생성
+kubectl apply -f ~/pkos/3/awsebs-pod.yaml
+
+# PVC, 파드 확인
+kubectl get pvc,pv,pod
+NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
+persistentvolumeclaim/ebs-claim Bound pvc-fb5b5e1c-76ef-4a43-9b94-9af2b1b1e5f7 4Gi RWO kops-csi-1-21 41m
+
+NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
+persistentvolume/pvc-fb5b5e1c-76ef-4a43-9b94-9af2b1b1e5f7 4Gi RWO Delete Bound default/ebs-claim kops-csi-1-21 40m
+
+NAME READY STATUS RESTARTS AGE
+pod/app 1/1 Running 0 3m15s
+
+# 실제 쌓이는 데이터 확인
+kubectl exec app -- tail -f /data/out.txt
+
+Sat Mar 18 14:49:25 UTC 2023
+Sat Mar 18 14:49:30 UTC 2023
+Sat Mar 18 14:49:35 UTC 2023
+...
+
+# 파드 내에서 볼륨 정보 확인
+kubectl exec -it app -- sh -c 'df -hT --type=ext4'
+Filesystem Type Size Used Avail Use% Mounted on
+/dev/nvme1n1 ext4 3.9G 16M 3.8G 1% /data
+/dev/root ext4 124G 4.9G 120G 4% /etc/hosts
+
+# 추가된 EBS 볼륨 상세 정보 확인
+while true; do aws ec2 describe-volumes --filters Name=tag:ebs.csi.aws.com/cluster,Values=true --query "Volumes[].{VolumeId: VolumeId, VolumeType: VolumeType, InstanceId: Attachments[0].InstanceId, State: Attachments[0].State}" --output text; date; sleep 1; done
+
+(중략)
+i-078613f7b7cd9e352 attached vol-071ebb777dc2ac3cd gp3 # 시간이 지난 후 추가되는 것 확인
+
# 현재 pv 의 이름을 기준하여 4G > 10G 로 증가 : .spec.resources.requests.storage의 4Gi 를 10Gi로 변경
+kubectl get pvc ebs-claim -o jsonpath={.spec.resources.requests.storage} ; echo
+kubectl get pvc ebs-claim -o jsonpath={.status.capacity.storage} ; echo
+kubectl patch pvc ebs-claim -p '{"spec":{"resources":{"requests":{"storage":"10Gi"}}}}'
+
+# 확인 : 볼륨 용량 수정 반영이 되어야 되니, 수치 반영이 조금 느릴수 있다
+kubectl exec -it app -- sh -c 'df -hT --type=ext4'
+kubectl df-pv
+aws ec2 describe-volumes --volume-ids $(kubectl get pv -o jsonpath="{.items[0].spec.csi.volumeHandle}") | jq
+
kubectl delete pod app & kubectl delete pvc ebs-claim
+
이번 글에서는 Kops 환경에서의 네트워크와 스토리지를 활용하는 방법을 정리해보았습니다.
일반적인 K8s와 달리 AWS 환경에서 EKS, Kops 등을 활용하게 된다면, AWS 리소스와의 연계를 통해 관리의 효율성과 편의성을 체감할 수 있었습니다.
다만, K8s만 알고 AWS는 잘 모르거나 또 그 반대의 상황에는 진입장벽이 있을 수 있겠다는 생각이 들었네요. 물론 이러한 부분만 해소된다면 관리형 쿠버네티스를 200% 활용할 수 있을 것 같습니다.
다음시간에는 GitOps와 관련된 주제로 찾아올 예정입니다.
이번에 연재할 스터디는 AWS EKS Workshop Study (=AEWS)이다. AWS에서 공식적으로 제공되는 다양한 HOL 기반의 Workshop과 가시다님의 팀에서 2차 가공한 컨텐츠를 기반으로 진행한다.
필자는 기본적인 스터디내용을 이번 시리즈에 연재할 예정이며, 추가적으로 HashiCorp의 Consul, Vault 등을 샘플로 배포하며 연동하는 내용을 조금씩 다뤄볼 예정이다.
참고 : AWS EKS 관련 핸즈온 워크샵을 해볼 수 있는 다양한 링크 모음이다.
여담이지만, HashiCorp 솔루션에 대한 다양한 HOL Workshop 실습도 사용자들이 많이 만들고 기여할 수 있도록 플랫폼을 오픈하면 좋을 것 같다.
Amazon Elastic Kubernetes Service는 자체 Kubernetes 컨트롤 플레인 또는 노드를 설치, 운영 및 유지 관리할 필요 없이 Kubernetes 실행에 사용할 수 있는 관리형 서비스이다.
EKS를 사용하는 다양한 이유가 있겠지만 대표적으로 여러 AWS 서비스와 통합할 수 있다는 장점이 크다.
다만 단점(?)이라고 할 수 있는 부분은 지원 버전이 보통 4개의 마이너 버전 지원(현재 1.22~1.26), 평균 3개월마다 새 버전 제공, 각 버전은 12개월 정도 지원한다는 것이다. 링크
이 부분이 단점이라고 이야기하는 이유는 실제 서비스를 운영하보면 EKS 클러스터 업그레이드를 하기위한 운영조직 체계가 갖춰져 있지 않은 상황에서 강제로 EKS 구버전에 대한 업그레이드를 해야하는 상황을 직면할 수 있기 때문이다.
물론, 보안, 서비스 지원 등 다양한 이유로 인해 클러스터 업그레이드는 불가피하지만 때때로 업그레이드를 강제해야하는 것은 특정 서비스를 운영하는 조직에게는 큰 걸림돌이 될 수 있다.
기회가 된다면 이번 스터디중에 EKS 업그레이드로 인한 어려움을 겪었던 사례를 발표해보려 한다.
참고 : EKS 업데이트 캘린더 : https://endoflife.date/amazon-eks
EKS 클러스터는 다음과 같은 방식으로 배포할 수 있다.
이번 EKS 스터디 시리즈에서는 eksctl
을 활용한 배포방식을 활용할 예정이다.
eksctl : EKS 클러스터 구축 및 관리를 하기 위한 오프소스 명령줄 도구 - 링크
실습환경은 외부에서 접근 가능한 Bastion 역할을 하는 EC2와 퍼블릭 서브넷 2개에 워커노드 두 대를 구성한다.
참고 : 실습환경 변경 챕터에서 노드를 3대로 증설예정
간단하게 VPC, Security Group, EC2 등을 구성하는 CF 코드를 통해 사전 환경을 구성한다.
aws cloudformation deploy --template-file myeks-1week.yaml \
+ --stack-name myeks --parameter-overrides KeyName=hw-key SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32 --region ap-northeast-2
+
+Waiting for changeset to be created..
+Waiting for stack create/update to complete
+Successfully created/updated stack - myeks
+
+# Public IP 확인
+aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[*].OutputValue' --output text
+13.124.14.182
+
+# ec2 에 SSH 접속
+ssh -i ~/.ssh/id_rsa ec2-user@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)
+
필자의 경우에는 AWS IAM 계정 정책에 대한 제약사항이 있어, aws configure
명령등으로 access_key
, secret_key
설정을 하지않고 EC2 인스턴스에 admin 권한을 부여하여 사용하였다.
eks-admin
이라고하는 admin 권한의 역할을 생성 후 부여# EKS 배포할 VPC 정보 확인
+export VPCID=$(aws ec2 describe-vpcs --filters "Name=tag:Name,Values=$CLUSTER_NAME-VPC" | jq -r .Vpcs[].VpcId)
+echo "export VPCID=$VPCID" >> /etc/profile
+echo VPCID
+
+## 퍼블릭 서브넷 ID 확인
+export PubSubnet1=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-PublicSubnet1" --query "Subnets[0].[SubnetId]" --output text)
+export PubSubnet2=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-PublicSubnet2" --query "Subnets[0].[SubnetId]" --output text)
+echo "export PubSubnet1=$PubSubnet1" >> /etc/profile
+echo "export PubSubnet2=$PubSubnet2" >> /etc/profile
+echo $PubSubnet1
+echo $PubSubnet2
+
+# EKS 클러스터 배포
+eksctl create cluster --name $CLUSTER_NAME --region=$AWS_DEFAULT_REGION --nodegroup-name=$CLUSTER_NAME-nodegroup --node-type=t3.medium --node-volume-size=30 --vpc-public-subnets "$PubSubnet1,$PubSubnet2" --version 1.24 --ssh-access --external-dns-access --verbose 4
+
EKS 클러스터를 명령형으로 배포하지 않고 YAML로 작성하여 언언적으로 배포하는 것도 가능하다. 다음은 앞서 실행한 eksctl create cluster
명령의 --dry-run
옵션을 통해 추출한 명세이다.
apiVersion: eksctl.io/v1alpha5
+cloudWatch:
+ clusterLogging: {}
+iam:
+ vpcResourceControllerPolicy: true
+ withOIDC: false
+kind: ClusterConfig
+kubernetesNetworkConfig:
+ ipFamily: IPv4
+managedNodeGroups:
+- amiFamily: AmazonLinux2
+ desiredCapacity: 2
+ disableIMDSv1: false
+ disablePodIMDS: false
+ iam:
+ withAddonPolicies:
+ albIngress: false
+ appMesh: false
+ appMeshPreview: false
+ autoScaler: false
+ awsLoadBalancerController: false
+ certManager: false
+ cloudWatch: false
+ ebs: false
+ efs: false
+ externalDNS: true
+ fsx: false
+ imageBuilder: false
+ xRay: false
+ instanceSelector: {}
+ instanceType: t3.medium
+ labels:
+ alpha.eksctl.io/cluster-name: myeks
+ alpha.eksctl.io/nodegroup-name: myeks-nodegroup
+ maxSize: 2
+ minSize: 2
+ name: myeks-nodegroup
+ privateNetworking: false
+ releaseVersion: ""
+ securityGroups:
+ withLocal: null
+ withShared: null
+ ssh:
+ allow: true
+ publicKeyPath: ~/.ssh/id_rsa.pub
+ tags:
+ alpha.eksctl.io/nodegroup-name: myeks-nodegroup
+ alpha.eksctl.io/nodegroup-type: managed
+ volumeIOPS: 3000
+ volumeSize: 30
+ volumeThroughput: 125
+ volumeType: gp3
+metadata:
+ name: myeks
+ region: ap-northeast-2
+ version: "1.24"
+privateCluster:
+ enabled: false
+ skipEndpointCreation: false
+vpc:
+ autoAllocateIPv6: false
+ cidr: 192.168.0.0/16
+ clusterEndpoints:
+ privateAccess: false
+ publicAccess: true
+ id: vpc-0521fc003559b2f2c
+ manageSharedNodeSecurityGroupRules: true
+ nat:
+ gateway: Disable
+ subnets:
+ public:
+ ap-northeast-2a:
+ az: ap-northeast-2a
+ cidr: 192.168.1.0/24
+ id: subnet-0fdff27653277aaf0
+ ap-northeast-2c:
+ az: ap-northeast-2c
+ cidr: 192.168.2.0/24
+ id: subnet-084a8752d4c7ddf6c
+
정상적으로 클러스터가 구성된 것을 확인할 수 있다.
# eks클러스터 확인
+eksctl get cluster
+NAME REGION EKSCTL CREATED
+myeks ap-northeast-2 True
+
+# 노드확인
+kubectl get node -v=6
+I0423 22:10:48.050969 2339 loader.go:374] Config loaded from file: /root/.kube/config
+
+I0423 22:10:48.880262 2339 round_trippers.go:553] GET https://6E205513BA73EEBC3CA693BADEEC5294.gr7.ap-northeast-2.eks.amazonaws.com/api/v1/nodes?limit=500 200 OK in 819 milliseconds
+NAME STATUS ROLES AGE VERSION
+ip-192-168-1-139.ap-northeast-2.compute.internal Ready <none> 61m v1.24.11-eks-a59e1f0
+ip-192-168-2-225.ap-northeast-2.compute.internal Ready <none> 61m v1.24.11-eks-a59e1f0
+
+# 파드확인
+k get pods -A
+NAMESPACE NAME READY STATUS RESTARTS AGE
+kube-system aws-node-2bpxr 1/1 Running 0 62m
+kube-system aws-node-s7p5b 1/1 Running 0 62m
+kube-system coredns-dc4979556-knkkh 1/1 Running 0 68m
+kube-system coredns-dc4979556-m789b 1/1 Running 0 68m
+kube-system kube-proxy-lkp8f 1/1 Running 0 62m
+kube-system kube-proxy-z6hbk 1/1 Running 0 62m
+
참고 :
eksctl
명령 예제
# eksctl help
+eksctl
+eksctl create
+eksctl create cluster --help
+eksctl create nodegroup --help
+
앞선 과정을 통해 실습을 위한 클러스터 구성이 완성되었다. 필자는 향후 샘플 애플리케이션으로 Vault, Consul 등을 배포할 예정이다. 때문에, 최소 3대 이상의 노드가 필요하여 기본 실습 노드를 3대로 구성한다.
EKS는 nodegraup
개수의 최소/최대 개수를 선언적으로 관리할 수 있다. 다음은 노드 개수를 변경/확인 하는 방법이다.
# eks 노드 그룹 정보 확인
+eksctl get nodegroup --cluster $CLUSTER_NAME --name $CLUSTER_NAME-nodegroup
+CLUSTER NODEGROUP STATUS CREATED MIN SIZE MAX SIZE DESIRED CAPACITY INSTANCE TYPE IMAGE ID ASG NAME TYPE
+myeks myeks-nodegroup UPDATING 2023-04-23T12:07:57Z 2 2 2 t3.medium AL2_x86_64 eks-myeks-nodegroup-fcc3d701-b90a-9c83-7907-fca8459770b9 managed
+
+# 노드 2개 → 3개 증가
+eksctl scale nodegroup --cluster $CLUSTER_NAME --name $CLUSTER_NAME-nodegroup --nodes 3 --nodes-min 3 --nodes-max 6
+
+# 노드 그룹 변경 확인
+eksctl get nodegroup --cluster myeks --region ap-northeast-2 --name myeks-nodegroup
+CLUSTER NODEGROUP STATUS CREATED MIN SIZE MAX SIZE DESIRED CAPACITY INSTANCE TYPE IMAGE ID ASG NAME TYPE
+myeks myeks-nodegroup UPDATING 2023-04-23T12:07:57Z 3 6 3 t3.medium AL2_x86_64 eks-myeks-nodegroup-fcc3d701-b90a-9c83-7907-fca8459770b9 managed
+
+# 노드 확인
+kubectl get nodes -o wide
+
+NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
+ip-192-168-1-139.ap-northeast-2.compute.internal Ready <none> 150m v1.24.11-eks-a59e1f0 192.168.1.139 43.201.51.34 Amazon Linux 2 5.10.176-157.645.amzn2.x86_64 containerd://1.6.19
+ip-192-168-1-76.ap-northeast-2.compute.internal Ready <none> 53s v1.24.11-eks-a59e1f0 192.168.1.76 13.124.158.208 Amazon Linux 2 5.10.176-157.645.amzn2.x86_64 containerd://1.6.19
+ip-192-168-2-225.ap-northeast-2.compute.internal Ready <none> 150m v1.24.11-eks-a59e1f0 192.168.2.225 52.79.236.227 Amazon Linux 2 5.10.176-157.645.amzn2.x86_64 containerd://1.6.19
+
+kubectl get nodes -l eks.amazonaws.com/nodegroup=$CLUSTER_NAME-nodegroup
+NAME STATUS ROLES AGE VERSION
+ip-192-168-1-139.ap-northeast-2.compute.internal Ready <none> 150m v1.24.11-eks-a59e1f0
+ip-192-168-1-76.ap-northeast-2.compute.internal Ready <none> 74s v1.24.11-eks-a59e1f0
+ip-192-168-2-225.ap-northeast-2.compute.internal Ready <none> 150m v1.24.11-eks-a59e1f0
+
필자는 본 글을 작성하던 시기에 고객사 환경에 ArgoCD + Helm을 기반으로 Consul Cluster 구성 테스트 요청이 있어 해당 클러스터를 활용해 보았다.
참고 : PKOS 2기에 사용한 ArgoCD 배포 가이드를 참고하여 배포한다.
OutOfSync
에러를 출력.# 네임스페이스 생성
+kubectl create ns argocd
+
+# argocd-helm 설치
+cd
+helm repo add argo https://argoproj.github.io/argo-helm
+helm repo update
+helm install argocd argo/argo-cd --set server.service.type=LoadBalancer --namespace argocd --version 5.19.14
+
+# 확인
+helm list -n argocd
+kubectl get pod,pvc,svc,deploy,sts -n argocd
+kubectl get-all -n argocd
+
+kubectl get crd | grep argoproj
+applications.argoproj.io 2023-03-19T11:39:26Z
+applicationsets.argoproj.io 2023-03-19T11:39:26Z
+appprojects.argoproj.io 2023-03-19T11:39:26Z
+
+# admin 계정의 암호 확인
+ARGOPW=$(kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d)
+echo $ARGOPW
+mf8bOtNEq7iHMqq1
+
+# 웹 접속 로그인 (admin) CLB의 hostname으로 접속
+k get svc -n argocd argocd-server -o jsonpath='{.status.loadBalancer.ingress[].hostname}'
+
필자는 개인 GitHub에 개인 퍼블릭 저장소를 만들어서 실습을 진행하였다.
사전에 로컬에서 다운로드 받은 Consul Helm Chart 파일에 새로 재정의 할 values 파일을 GitHub 저장소에 업로드 한다.
참고 : 다음은 실제 작성된 Values 파일이다. 각 항목에 대한 상세한 설명은 향후 Consul Deploy on K8s 가이드를 작성해서 업로드 예정이다.
client:
+ grpc: true
+connectInject:
+ consulNamespaces:
+ mirroringK8S: true
+ enabled: true
+controller:
+ enabled: true
+global:
+ acls:
+ manageSystemACLs: true
+ enableConsulNamespaces: true
+ enterpriseLicense:
+ secretKey: key
+ secretName: license
+ gossipEncryption:
+ autoGenerate: true
+ image: hashicorp/consul-enterprise:1.13.7-ent
+ imageEnvoy: envoyproxy/envoy:v1.22.5
+ imageK8S: hashicorp/consul-k8s-control-plane:0.49.5
+ metrics:
+ enabled: false
+ tls:
+ enableAutoEncrypt: true
+ enabled: true
+ httpsOnly: false
+ verify: false
+ingressGateways:
+ defaults:
+ replicas: 1
+ service:
+ type: LoadBalancer
+ enabled: true
+ gateways:
+ - name: ingress-gateway
+meshGateway:
+ enabled: false
+ replicas: 1
+ service:
+ enabled: true
+ nodePort: 32000
+ type: NodePort
+server:
+ replicas: 3
+terminatingGateways:
+ defaults:
+ replicas: 1
+ enabled: false
+ui:
+ enabled: true
+ service:
+ port:
+ http: 80
+ https: 443
+ type: LoadBalance
+
ArgoCD CLI를 통해 필자의 GitHub을 연동한다
argocd login <argocd 주소> --username admin --password $ARGOPW
+...
+'admin:login' logged in successfully
+
+argocd repo add https://github.com/<깃헙 계정명>/<레파지토리명> --username <깃헙 계정명> --password <깃헙 계정 암호>
+
+argocd repo list
+TYPE NAME REPO INSECURE OCI LFS CREDS STATUS MESSAGE PROJECT
+git https://github.com/chosam2/gitops.git false false false true Successful
+
+# 기본적으로 아르고시디가 설치된 쿠버네티스 클러스터는 타깃 클러스터로 등록됨
+argocd cluster list
+SERVER NAME VERSION STATUS MESSAGE PROJECT
+https://kubernetes.default.svc in-cluster 1.24+ Successful
+
앞서 생성한 GitHub 저장소에 업로드한 consul helm values 파일을 통해 배포하기 위해 Application
CRD를 생성 및 배포한다.
consul-helm-argo-application.yaml
apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name: consul-helm
+ namespace: argocd
+ finalizers:
+ - resources-finalizer.argocd.argoproj.io
+spec:
+ destination:
+ namespace: consul
+ server: https://kubernetes.default.svc
+ project: default
+ source:
+ repoURL: https://github.com/chosam2/gitops.git
+ path: argocd
+ targetRevision: main
+ helm:
+ valueFiles:
+ - override-values.yaml
+ syncPolicy:
+ syncOptions:
+ - CreateNamespace=true
+
k apply -f consul-helm-argo-application.yaml
+application.argoproj.io/consul-helm created
+
최초 배포 시 OutOfSync
상태로 배포되었지만, 동기화 버튼을 클릭하여 강제로 동기화해준 뒤 정상적으로 배포된 것을 확인할 수 있다.
다만, 최초 OutOfSync
상태로 배포되는 부분에 대해서는 Application YAML 작성 시 옵션을 통해 해결이 가능하지만, 실제 운영시 영향도 체크가 필요해 보인다. 이 부분은 다음 블로깅시에 조금 더 테스트 및 확인해볼 예정이다.
1주차 스터디는 EKS에 대한 전반적인 컨셉과 기본적으로 클러스터를 구성하고 Consul Cluster를 간단하게 배포해보았다. 다음주에는 네트워킹을 주제로 찾아올 예정이다.
이번에 연재할 스터디는 AWS EKS Workshop Study (=AEWS)이다. AWS에서 공식적으로 제공되는 다양한 HOL 기반의 Workshop과 가시다님의 팀에서 2차 가공한 컨텐츠를 기반으로 진행한다.
2주차 부터는 원클릭으로 EKS 실습환경을 배포할 수 있는 코드를 사용한다. 필자는 사용중인 AWS IAM 권한 제약사항으로 기존 CF 코드를 변경하여 베스천용 EC2에 관리자 권한을 위임하여 배포할 예정이다.
참고 : https://cloudkatha.com/attach-an-iam-role-to-an-ec2-instance-with-cloudformation/
# YAML 파일 다운로드
+curl -O https://gist.githubusercontent.com/hyungwook0221/238d96b3b751362cc03ea40494d15313/raw/49de0a9056688b206a41349fc90727d2375f4f02/aews-eks-oneclick-with-ec2-profile.yaml
+
+# CloudFormation 스택 배포
+# aws cloudformation deploy --template-file eks-oneclick.yaml --stack-name myeks --parameter-overrides KeyName=<My SSH Keyname> SgIngressSshCidr=<My Home Public IP Address>/32 MyIamUserAccessKeyID=<IAM User의 액세스키> MyIamUserSecretAccessKey=<IAM User의 시크릿 키> ClusterBaseName='<eks 이름>' --region ap-northeast-2
+예시) aws cloudformation deploy --template-file eks-oneclick.yaml --stack-name myeks --parameter-overrides KeyName=hw-key SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32 ClusterBaseName=myeks --region ap-northeast-2 --capabilities CAPABILITY_NAMED_IAM
+
+# CloudFormation 스택 배포 완료 후 작업용 EC2 IP 출력
+aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text
+
+# 마스터노드 SSH 접속
+ssh -i ~/.ssh/kp-gasida.pem ec2-user@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)
+
일반적으로 Calico와 같은 K8s CNI의 경우는 Node - Pod의 IP 대역이 다르지만 AWS VPC CNI의 경우에는 Node-Pod 대역을 동일하게 해서 통신이 가능하도록 구성할 수 있다.
일반적으로 Outer 패킷을 감싸서 오버레이로 통신하지만 AWS VPC CNI는 오히려 심플한 구조를 가진다. 이로인해 간단하고 효율적인 통신이 가능하다!
K8s 환경에서는 내/외부 통신을 위한 서비스를 크게 3가지 형태로 제공한다.
필자는 그 중에서 LoadBalancer 타입을 AWS 환경에서 어떻게 활용할 수 있는지를 집중적으로 확인하고 Consul 샘플 예제와 함께 적용해볼 예정이다.
LoadBalancer 배포 시 NLB 모드는 다음 두 가지 유형을 사용할 수 있다.
externalTrafficPolicy
: ClusterIP ⇒ 2번 분산 및 SNAT으로 Client IP 확인 불가능 ← LoadBalancer
타입 (기본 모드) 동작externalTrafficPolicy
: Local ⇒ 1번 분산 및 ClientIP 유지, 워커 노드의 iptables 사용함참고 : 반드시 AWS LoadBalancer 컨트롤러 파드 및 정책 설정이 필요함!
Proxy Protocol v2 비활성화
⇒ NLB에서 바로 파드로 인입, 단 ClientIP가 NLB로 SNAT 되어 Client IP 확인 불가능Proxy Protocol v2 활성화
⇒ NLB에서 바로 파드로 인입 및 ClientIP 확인 가능(→ 단 PPv2 를 애플리케이션이 인지할 수 있게 설정 필요)# AWSLoadBalancerControllerIAMPolicy 생성
+curl -o iam_policy.json https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.4.7/docs/install/iam_policy.json
+aws iam create-policy --policy-name AWSLoadBalancerControllerIAMPolicy --policy-document file://iam_policy.json
+
+# 업데이트가 필요한 경우
+# aws iam update-policy --policy-name AWSLoadBalancerControllerIAMPolicy --policy-document file://iam_policy.json
+
+# AWS Load Balancer Controller를 위한 ServiceAccount를 생성
+eksctl create iamserviceaccount --cluster=$CLUSTER_NAME --namespace=kube-system --name=aws-load-balancer-controller \
+--attach-policy-arn=arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy --override-existing-serviceaccounts --approve
+
+## IRSA 정보 확인
+eksctl get iamserviceaccount --cluster $CLUSTER_NAME
+
+## 서비스 어카운트 확인
+kubectl get serviceaccounts -n kube-system aws-load-balancer-controller -o yaml | yh
+
+# Helm Chart 설치
+helm repo add eks https://aws.github.io/eks-charts
+helm repo update
+helm install aws-load-balancer-controller eks/aws-load-balancer-controller -n kube-system --set clusterName=$CLUSTER_NAME \
+ --set serviceAccount.create=false --set serviceAccount.name=aws-load-balancer-controller
+
+# 설치 확인
+kubectl get crd
+kubectl get deployment -n kube-system aws-load-balancer-controller
+kubectl describe deploy -n kube-system aws-load-balancer-controller | grep 'Service Account'
+ Service Account: aws-load-balancer-controller
+
+# 클러스터롤, 클러스터 롤바인딩 확인
+kubectl describe clusterrolebindings.rbac.authorization.k8s.io aws-load-balancer-controller-rolebinding
+kubectl describe clusterroles.rbac.authorization.k8s.io aws-load-balancer-controller-role
+
AWS LoadBalancer Controller가 동작하기 위해 필요한 SA를 생성 후 연결된 ClusterRole과 ClusterRoleBinding을 화인
LoadBalancer 타입의 서비스와 및 파드를 배포하고 NLB 모드에 따라서 Client IP가 어떻게 확인되는지 확인해본다.
# 모니터링
+watch -d kubectl get pod,svc,ep
+
+# 작업용 EC2 - 디플로이먼트 & 서비스 생성
+curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/2/echo-service-nlb.yaml
+cat echo-service-nlb.yaml | yh
+kubectl apply -f echo-service-nlb.yaml
+
+# 파드 로깅 모니터링
+kubectl logs -l app=deploy-websrv -f
+
+# 분산 접속 확인
+NLB=$(kubectl get svc svc-nlb-ip-type -o jsonpath={.status.loadBalancer.ingress[0].hostname})
+curl -s $NLB
+
NLB에 등록된 Target IP 정보는 생성된 샘플 Pod의 IP인 것을 확인할 수 있다.
이제 NLB를 통해서 Pod를 호출할 경우 Client IP가 어떻게 확인되는지 확인해보자.
다음 정보는 각 Node의 정보가 아닌 다른 IP 정보가 확인된다.
그렇다면 Client IP의 정체는? 바로 NLB에 할당된 네트워크 인터페이스의 IP 이다.
이제 실제로 Client IP를 추적하기 위한 방법을 알아본다.
앞선 실습에서 NLB로 SNAT되어서Client IP 확인되지 못하는 것을 확인하였다. 이번에는 Proxy Protocol v2을 활성화 하여 IP 정보를 유지하는 방법을 알아본다. (이미지 출처 : 가시다님 스터디)
이때 중요한 부분은 SVC 생성 시 service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: "*"
어노테이션을 활성화 하는 것이다.
# 생성
+cat <<EOF | kubectl create -f -
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: gasida-web
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: gasida-web
+ template:
+ metadata:
+ labels:
+ app: gasida-web
+ spec:
+ terminationGracePeriodSeconds: 0
+ containers:
+ - name: gasida-web
+ image: gasida/httpd:pp
+ ports:
+ - containerPort: 80
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: svc-nlb-ip-type-pp
+ annotations:
+ service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
+ service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
+ service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
+ service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: "*"
+spec:
+ ports:
+ - port: 80
+ targetPort: 80
+ protocol: TCP
+ type: LoadBalancer
+ loadBalancerClass: service.k8s.aws/nlb
+ selector:
+ app: gasida-web
+EOF
+
+---
+
+# apache에 proxy protocol 활성화 확인
+kubectl exec deploy/gasida-web -- apachectl -t -D DUMP_MODULES
+kubectl exec deploy/gasida-web -- cat /usr/local/apache2/conf/httpd.conf
+
+# 접속 확인
+NLB=$(kubectl get svc svc-nlb-ip-type-pp -o jsonpath={.status.loadBalancer.ingress[0].hostname})
+curl -s $NLB
+
+# 지속적인 접속 시도
+while true; do curl -s --connect-timeout 1 $NLB; echo "----------" ; date "+%Y-%m-%d %H:%M:%S" ; sleep 1; done
+
+# 로그 확인
+kubectl logs -l app=gasida-web -f
+
IP를 확인해본 결과 동일한 공인 IP로 찍히는 것으로 확인된다.
그렇다면 해당 IP는 무엇일까? 바로 현재 curl -s
명령을 수행한 Bastion 노드의 정보이다.
이렇게 NLB를 통해 호출하더라도 정상적으로 Client IP를 유지하는 방법을 알아보았다. 실제로 온프레미스 환경에서 3-Tier 기반의 WEB/WAS를 구성하다 보면 Client IP를 유지하기 위해 XFF 설정을 하는 것이 일반적이다. 다만, NLB의 경우에는 L4 계층까지만 패킷에 대한 이해와 분석이 가능하므로 Proxy Protocol을 써야한다는 새로운 정보를 알 수 있는 좋은 기회였다.
다음 예제는 Consul IngressGateway를 통한 ServiceMesh의 단일 진입점을 테스트해볼 예정이다. Consul 1.15x 버전에는 Envoy의 Access Log 기능이 추가되어 이번 스터디를 통해 학습한 NLB의 IP 유지방안에 대한 테스트를 진행해본다.
참고 : Consul Gateway에서 envoy access log 활성화 기능
처음 설정시에는 PPv2를 사용하지 않고 NLB를 적용해볼 예정이다. => Client IP가 어떻게 찍히는지 확인!
client:
+ grpc: true
+connectInject:
+ consulNamespaces:
+ mirroringK8S: true
+ enabled: true
+controller:
+ enabled: true
+global:
+ acls:
+ manageSystemACLs: true
+ enableConsulNamespaces: true
+ enterpriseLicense:
+ secretKey: key
+ secretName: license
+ gossipEncryption:
+ autoGenerate: true
+ image: hashicorp/consul-enterprise:1.15.1-ent
+ #imageEnvoy: envoyproxy/envoy:v1.22.5
+ #imageK8S: hashicorp/consul-k8s-control-plane:0.49.5
+ metrics:
+ enabled: false
+ tls:
+ enableAutoEncrypt: true
+ enabled: true
+ httpsOnly: false
+ verify: false
+ingressGateways:
+ defaults:
+ replicas: 1
+ service:
+ type: LoadBalancer
+ annotations: |
+ service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
+ service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
+ service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
+ #service.beta.kubernetes.io/aws-load-balancer-healthcheck-port: "8080"
+ enabled: true
+ gateways:
+ - name: ingress-gateway
+meshGateway:
+ enabled: false
+ replicas: 1
+ service:
+ enabled: true
+ nodePort: 32000
+ type: NodePort
+server:
+ replicas: 3
+terminatingGateways:
+ defaults:
+ replicas: 1
+ enabled: false
+ui:
+ enabled: true
+ service:
+ port:
+ http: 80
+ https: 443
+ type: LoadBalancer
+
apiVersion: consul.hashicorp.com/v1alpha1
+kind: IngressGateway
+metadata:
+ name: ingress-gateway
+spec:
+ listeners:
+ - port: 8080
+ protocol: http
+ services:
+ - name: static-server
+
spec.accessLogs
를 통해 AccessLog 활성화 및 파일경로 추가
apiVersion: consul.hashicorp.com/v1alpha1
+kind: ProxyDefaults
+metadata:
+ name: global
+spec:
+ accessLogs:
+ enabled: true
+# type: file
+# path: "/var/log/envoy/access-logs.txt"
+
apiVersion: consul.hashicorp.com/v1alpha1
+kind: ServiceDefaults
+metadata:
+ name: static-server
+spec:
+ protocol: http
+
apiVersion: consul.hashicorp.com/v1alpha1
+kind: ServiceIntentions
+metadata:
+ name: static-server
+spec:
+ destination:
+ name: static-server
+ sources:
+ - name: ingress-gateway
+ action: allow
+
apiVersion: v1
+kind: Service
+metadata:
+ name: static-server
+spec:
+ selector:
+ app: static-server
+ ports:
+ - protocol: TCP
+ port: 80
+ targetPort: 8080
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: static-server
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: static-server
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: static-server
+ template:
+ metadata:
+ name: static-server
+ labels:
+ app: static-server
+ annotations:
+ 'consul.hashicorp.com/connect-inject': 'true'
+ spec:
+ containers:
+ - name: static-server
+ image: hashicorp/http-echo:latest
+ args:
+ - -text="hello world"
+ - -listen=:8080
+ ports:
+ - containerPort: 8080
+ name: http
+ serviceAccountName: static-server
+
EXTERNAL_IP=$(kubectl get services --selector component=ingress-gateway --output jsonpath="{range .items[*]}{@.status.loadBalancer.ingress[*].hostname}{end}")
+echo "Connecting to \"$EXTERNAL_IP\""
+curl --header "Host: static-server.ingress.consul" "http://$EXTERNAL_IP:8080"
+
호출결과 앞서 실습에서 확인해본 것과 동일하게 NLB IP Target & Proxy Protocol v2 비활성화 일 경우에는 로드밸런서 인터페이스 IP가 확인된다.
이번에는 위와 동일하지만 NLB의 어노테이션만 PPv2를 활성화 한다.
service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: "*"
#(생략)
+ingressGateways:
+ defaults:
+ replicas: 1
+ service:
+ type: LoadBalancer
+ annotations: |
+ service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
+ service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
+ service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
+ service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: "*"
+ enabled: true
+ gateways:
+ - name: ingress-gateway
+#(생략)
+
위와 동일하게 사용
EXTERNAL_IP=$(kubectl get services --selector component=ingress-gateway --output jsonpath="{range .items[*]}{@.status.loadBalancer.ingress[*].hostname}{end}")
+echo "Connecting to \"$EXTERNAL_IP\""
+curl --header "Host: static-server.ingress.consul" "http://$EXTERNAL_IP:8080"
+
하지만 PPv2 설정 후 static-server 앱을 테스트해본 결과 정상적으로 동작하지 않는 것으로 보인다.
위와 관련해서 확인해본 결과 Istio의 경우에는 EnvoyFilter
등을 통해 해결하는 방안(?)이 있는 것으로 보이며, 일반적으로 PPv2를 사용하기 위해서는 애플리케이션 단에서 사용할 수 있도록 설정이 필요한 것으로 확인되었다.
참고 :
📌 정보공유:
해당 이슈에 대하여 Consul Product Manager를 통해 FR(Feture Request)로 등록 후 신규 기능으로 추가할 수 있도록 지원할 것으로 답변받았다. 추후 업데이트에 대한 변동사항이 있으면 할 예정이다.
위에서 언급한 것 처럼 Consul Native하게는 PPv2 기능을 사용하기 어려운 상황이라 Apache 애플리케이션에서 PPv2 설정을 통해 해결이 가능한지 확인을 해보았다.
확인결과 결과적으로 테스트가 불가능 한 것으로 확인되었다. Apache 애플리케이션에 PPv2를 활성화 하고 Consul CRD를 적용하여 IngressGateway에서 호출하였으나, 400 에러가 발생한다. (NLB PPv2 활성화 시 발생)
아쉽지만 본 테스트는 FR이 진행된 이후에 업데이트 하도록 하겠다.
원문 : https://blog.openshift.com/deploying-applications-to-specific-nodes/
Deployment나 Deployment Config에서 Nodeselect를 지정하는 방법 외에 Project 단위로 설정하는 방법을 설명합니다.
Node Label확인
~$ oc get node --show-labels
+
Node에 Label 업데이트
Label 업데이트 (링크)
~$ oc label node [노드이름] region=primary
+
primary는 구분자 입니다. 예로
region=tk
이런식으로 테스트계 설정을 하게 됩니다.
ex)
To add a label to a node or pod:
# oc label node node001.krenger.ch mylabel=myvalue
+# oc label pod mypod-34-g0f7k mylabel=myvalue
+
To remove a label (in the example “mylabel”) from a node or pod:
# oc label node node001.krenger.ch mylabel-
+# oc label pod mypod-34-g0f7k mylabel-
+
Master에 기본 NodeSelector Label 지정
Master 노드의 defaultNodeSelector
설정을 변경하고 Master서비스를 재시작 합니다.
NodeSelector
가 없는 경우 Pod가 Deploy되는 Node 입니다.
대상 파일: /etc/origin/master/master-config.yaml
projectConfig:
+ defaultNodeSelector: "region=primary”
+
Master Node 재기동 필요
~$ systemctl restart atomic-openshift-master
+~$ systemctl restart atomic-openshift-node
+
프로젝트 Node Selector 설정
~$ oc edit namespace [프로젝트이름]
+
...
+ annotations:
+ openshift.io/node-selector: “region=primary"
+ openshift.io/description: ""
+ openshift.io/display-name: ""
+ openshift.io/node-selector: “"
+...
+
Appendix
Autoscaling applications using custom metrics on OpenShift Container Platform 3.11 with JBoss EAP or Wildfly
Red Hat OpenShift Container Platform 3.11 (OCP) 은 기본적으로 CPU에 대한 애플리케이션 자동 확장을 지원합니다. 추가적으로 apis/autoscaling/v2beta1
를 활성화하여 Memory의 메트릭을 기반으로 한 기능도 지원 합니다. CPU나 Memory의 경우 애플리케이션에 종속되지 않은 기본적인 메트릭이나, 때로는 추가적인 메트릭 요소를 기반으로 확장할 필요성이 있습니다.
Prometheus Adaptor를 사용하면 기본 메트릭 외에도 사용자가 지정한 애플리케이션의 메트릭을 기반으로 자동확장하는 기능을 추가 할 수 있습니다.
Prometheus Adaptor는 OCP 4.1 부터 에서 Tech Preview 대상이 되었습니다. 기능이 완전해지면 정식 지원상태로 변경 될 것입니다.
Prometheus Adaptor는 custom.metrics.k8s.io
API를 구현하여 Prometheus에 연결합니다. Prometheus에서 수집되는 메트릭 기반으로 Horizontal Pod Autoscaler (HPA)가 쿼리하여 애플리케이션 메트릭을 검색할 수 있습니다.
이글은 다음의 과정을 안내 합니다.
각 내용의 상세 정보는 참고 자료의 내용이 도움이 됩니다.
cluster-admin
권한이 있는 계정OpenShift Container Platform 3.11 환경에 Operator를 활성화 하기 위한 작업을 수행합니다. 기존에 이미 Operator 구성을 설치 한경우 해당 과정을 넘어갑니다.
OCP 3.11 설치를 진행한 hosts 파일의 [OSEv3:vars]
항목에 다음을 추가
registry.connect.redhat.com
에 접속하기 위한 계정 정보가 필요
openshift_additional_registry_credentials=[{'host':'registry.connect.redhat.com','user':'<your_user_name>','password':'<your_password>','test_image':'mongodb/enterprise-operator:0.3.2'}]
+
registry 추가를 위한 Ansible playbook 실행
$ cd /usr/share/ansible/openshift-ansible
+$ ansible-playbook -i <inventory_file> playbooks/updates/registry_auth.yml
+
Operator framework 설치를 위한 Ansible playbook 실행
$ cd /usr/share/ansible/openshift-ansible
+$ ansible-playbook -i <inventory_file> playbooks/olm/config.yml
+
Operator framework가 설치되면 Cluster Console 에서 좌측 메뉴에 추가된 Operators
를 확인 할 수 있습니다.
JMX export는 Prometheus로 Java 기반의 애플리케이션에서 수집 가능한 JMX의 mBean을 전달가능하도록 하는 수집기 입니다. Java 애플리케이션과 함께 실행되며 HTTP 엔드포인트를 노출시켜 JVM의 메트릭 정보를 제공합니다.
JMX export를 javaagent 방식으로 적용하면 애플리케이션에 별도의 수정이나 추가 코딩 없이 JMX로 수집되는 mBean 값들을 노출 시킬 수 있습니다.
다음의 샘플 애플리케이션을 기반으로 설명합니다.
https://github.com/Great-Stone/webapp
애플리케이션 구조는 다음과 같습니다.
webapp
+├ configuration
+│ ├ standalone-openshift.xml
+│ └ jmx_exporter_conf.yaml
+├ modules
+│ └ jmx_prometheus_javaagent-0.12.0.jar
+└ ROOT.war
+
Red Hat에서 제공되는 JBoss EAP는 S2I 빌드 배포를 지원합니다. 애플리케이션 소스 또는 바이너리를 별도의 이미지 빌드(e.g. Docker build) 없이 바로 OpenShift 상에서 사용 가능한 컨테이너 이미지로 빌드하는 기능입니다.
jmx_exporter_conf.yaml
파일을 해당 디렉토리에 위치시켜 S2I 빌드시 빌드 이미지 내에 복사되도록 합니다.jmx_prometheus_javaagent-0.12.0.jar
를 빌드 시 이미지 내부에 복사 할 수 있도록 해당 디렉토리내에 위치시킵니다.jmx_exporter_conf.yaml
에서 예시로 설정한 내용은 다음과 같습니다.
---
+startDelaySeconds: 30
+lowercaseOutputName: true
+lowercaseOutputLabelNames: true
+whitelistObjectNames:
+ - "jboss.as:subsystem=request-controller"
+rules:
+ - pattern: "^jboss.as<subsystem=request-controller><>(active_.+|max_.+): (.*)"
+ attrNameSnakeCase: true
+ name: jboss_$1
+ help: "jboss Request Controller : $1"
+ labels:
+ namespace: 'my-eap-project'
+ pod: 'sample-eap'
+ service: 'sample-eap'
+
Name | Description |
---|---|
whitelistObjectNames | "jboss.as:subsystem=request-controller" 의 경우 mBean 값을 기준으로 확인 가능합니다. jconsole이나 jvisualVM 툴을 사용하여 확인 가능하며, 필요시 로컬이나 리모트의 JBoss EAP / Wildfly에서 원하는 값 정의 가능 |
pattern | mBean ObjectName의 Attribute 값의 패턴을 정의 합니다. 단일 또는 복수의 Name을 정의 가능 |
name | jmx_exporter 에서 표기할 이름 규칙을 설정 |
labels | OpenShift 환경에서 식별할 수 있는 label을 추가합니다. namespace, pod, service 는 기본 JMX exporter로는 수집되지 않으므로 해당 애플리케이션이 배포될 OpenShift 환경에 맞춰 설정 |
기타 상세 옵션은 다음의 url에서 확인 가능합니다.
https://github.com/prometheus/jmx_exporter#configuration
OpenShift Application Console에서 작업 진행
배포 할 프로젝트를 생성 (e.g. my-eap-project)
해당 프로젝트를 선택 후 좌측 메뉴의 Catalog를 선택
JBoss EAP 7.2
를 선택
Next>
클릭jmx_exporter_conf.yaml
의 레이블 설정 참고) Create
버튼 클릭Close
버튼 클릭좌측 메뉴에서 Builds
클릭 후 생성한 sample-eap
확인
좌측 메뉴에서 Applications
> Deployments
클릭 후 생성된 Deployment Config sample-eap
클릭
Environment 탭을 선택하고 다음을 추가하고 하단 Save
버튼 클릭
Name | Value |
---|---|
JAVA_OPTS_APPEND | -javaagent:/opt/eap/modules/jmx_prometheus_javaagent-0.12.0.jar=58080:/opt/eap/standalone/configuration/jmx_exporter_conf.yaml |
JAVA_OPTS_APPEND
환경 변수에 값을 기입하면 해당 JBoss EAP 7.2 S2I 빌드 시 실행되는 서버의 Java Option 값 뒤에 해당 값이 추가됨
javaagent
로 빌드시 해당 컨테이너 이미지 내부로 복사된 modules
디렉토리의 jmx_prometheus_javaagent-0.12.0.jar
를 지정하고 =58080
은 엔드포인트 포트를 정의
추가로 컨테이너 이미지 내부로 복사된 configuration
디렉토리의 설정 파일인 jmx_exporter_conf.yaml
을 정의
우측 상단의 Actions
> Edit YAML
을 클릭하여 spec > template > metadata > annotations
에 prometheus.io/scrape: 'true'
를 추가하고 하단의 Save
버튼 클릭
apiVersion: apps.openshift.io/v1
+kind: DeploymentConfig
+...
+spec:
+ ...
+ template:
+ metadata:
+ annotations:
+ prometheus.io/scrape: 'true'
+ creationTimestamp: null
+ ...
+
좌측 메뉴에서 Applications
> Services
클릭 후 생성된 Service sample-eap
클릭
우측 상단의 Actions
> Edit YAML
을 클릭하여 prometheus.io/scrape: "true"
항목과 export를 위한 port를 추가하고 하단 Save
버튼을 클릭하여 저장합니다. port는 eap를 위한 서비스를 위한 port와 exporter를 위한 port 두개가 필요함
apiVersion: v1
+kind: Service
+metadata:
+ annotations:
+ description: The web server's http port.
+ prometheus.io/scrape: 'true'
+ ...
+spec:
+ ...
+ ports:
+ - name: eap
+ port: 8080
+ protocol: TCP
+ targetPort: 8080
+ - name: exporter
+ port: 58080
+ protocol: TCP
+ targetPort: 58080
+ ...
+
Service에 새로운 포트를 추가하면 기존 route를 올바른 포트에 연결하고, exporter의 데이터 확인을 위한 새로운 route를 다음 단계에서 추가
좌측 메뉴에서 Applications
> Routes
클릭 후 생성된 route sample-eap
클릭
Actions
> Edit
을 클릭 하고 Target Port
를 8080→8080(TCP)
임을 확인 후 하단 Save
버튼 클릭Create Route
클릭하여 다음을 설정하고 하단 Save
버튼 클릭 JMX exporter가 적용되었는지 확인을 위해 사로 생성한 sample-eap-exporter
의 Hostname을 클릭하여 정보 확인
...
+# HELP jboss_active_requests jboss Request Controller : active_requests
+# TYPE jboss_active_requests untyped
+jboss_active_requests{namespace="my-eap-project",pod="sample-eap",service="sample-eap",} 0.0
+# HELP jboss_max_requests jboss Request Controller : max_requests
+# TYPE jboss_max_requests untyped
+jboss_max_requests{namespace="my-eap-project",pod="sample-eap",service="sample-eap",} -1.0
+...
+
jmx_exporter_conf.yaml
에서 정의한 JMX 내용이 표기되는지 확인jboss_
를 prefix로 정의하였고 active_*
, max_*
항목의 Attribute 데이터를 표기Operator framework를 활용하여 애플리케이션을 모니터링 하도록 설정할 수 있습니다. 각 yaml로 작성된 설정은 CLI 또는 OpenShift Console 상에서 진행 할 수 있습니다. 적용하는 각 방법은 OpenShift에 리소스 배포 를 참고하세요.
Prometheus 구성요소를 배포하기 위해 프로젝트를 구성합니다. 여기서는 custom-metric
프로젝트에서 진행합니다.
$ oc create -f - <<EOF
+apiVersion: operators.coreos.com/v1alpha1
+kind: Subscription
+metadata:
+ generateName: prometheus-
+ namespace: custom-metric
+spec:
+ source: rh-operators
+ name: prometheus
+ startingCSV: prometheusoperator.0.22.2
+ channel: preview
+EOF
+
oc apply -f - <<EOF
+apiVersion: monitoring.coreos.com/v1
+kind: ServiceMonitor
+metadata:
+ name: sample-app
+ labels:
+ app: sample-app
+ namespace: prometheus-metric
+spec:
+ selector:
+ matchLabels:
+ app: sample-app
+ namespaceSelector:
+ matchNames:
+ - my-eap-project
+ endpoints:
+ - port: exporter
+EOF
+
oc apply -f - <<EOF
+apiVersion: monitoring.coreos.com/v1
+kind: Prometheus
+metadata:
+ name: example
+ labels:
+ prometheus: k8s
+ namespace: prometheus-metric
+spec:
+ replicas: 1
+ version: v2.3.2
+ serviceAccountName: prometheus-k8s
+ securityContext: {}
+ serviceMonitorSelector:
+ matchLabels:
+ app: sample-app
+EOF
+
$ oc apply -f - <<EOF
+apiVersion: v1
+kind: Service
+metadata:
+ name: prometheus
+spec:
+ ports:
+ - name: web
+ port: 9090
+ protocol: TCP
+ targetPort: web
+ selector:
+ prometheus: example
+EOF
+
Prometheus가 설정된 상태에서 다음 리소스에 대한 설정을 추가하여 Prometheus Adopter 설정합니다. RBAC 접근제어, Adapter 구성, API 확장, Deployment 항목들이 포함되어있습니다.
kind: ServiceAccount
+apiVersion: v1
+metadata:
+ name: custom-metrics-apiserver
+ namespace: prometheus-metric
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: custom-metrics-server-resources
+rules:
+- apiGroups:
+ - custom.metrics.k8s.io
+ resources: ["*"]
+ verbs: ["*"]
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: custom-metrics-resource-reader
+rules:
+- apiGroups:
+ - ""
+ resources:
+ - namespaces
+ - pods
+ - services
+ verbs:
+ - get
+ - list
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: custom-metrics:system:auth-delegator
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: system:auth-delegator
+subjects:
+- kind: ServiceAccount
+ name: custom-metrics-apiserver
+ namespace: prometheus-metric
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: custom-metrics-auth-reader
+ namespace: kube-system
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: extension-apiserver-authentication-reader
+subjects:
+- kind: ServiceAccount
+ name: custom-metrics-apiserver
+ namespace: prometheus-metric
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: custom-metrics-resource-reader
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: custom-metrics-resource-reader
+subjects:
+- kind: ServiceAccount
+ name: custom-metrics-apiserver
+ namespace: prometheus-metric
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: hpa-controller-custom-metrics
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: custom-metrics-server-resources
+subjects:
+- kind: ServiceAccount
+ name: horizontal-pod-autoscaler
+ namespace: prometheus-metric
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: adapter-config
+ namespace: prometheus-metric
+data:
+ config.yaml: |
+ rules:
+ - seriesQuery: '{__name__="jboss_active_requests",namespace!="",pod!=""}'
+ resources:
+ overrides:
+ namespace: {resource: "namespace"}
+ pod: {resource: "pod"}
+ service: {resource: "service"}
+ name:
+ matches: "^(.*)_requests"
+ as: "${1}_avg"
+ metricsQuery: '<<.Series>>{<<.LabelMatchers>>}'
+---
+apiVersion: v1
+kind: Service
+metadata:
+ annotations:
+ service.alpha.openshift.io/serving-cert-secret-name: prometheus-adapter-tls
+ labels:
+ name: prometheus-adapter
+ name: prometheus-adapter
+ namespace: prometheus-metric
+spec:
+ ports:
+ - name: https
+ port: 443
+ targetPort: 6443
+ selector:
+ app: prometheus-adapter
+ type: ClusterIP
+---
+apiVersion: apiregistration.k8s.io/v1beta1
+kind: APIService
+metadata:
+ name: v1beta1.custom.metrics.k8s.io
+spec:
+ service:
+ name: prometheus-adapter
+ namespace: prometheus-metric
+ group: custom.metrics.k8s.io
+ version: v1beta1
+ insecureSkipTLSVerify: true
+ groupPriorityMinimum: 100
+ versionPriority: 100
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ labels:
+ app: prometheus-adapter
+ name: prometheus-adapter
+ namespace: prometheus-metric
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: prometheus-adapter
+ template:
+ metadata:
+ labels:
+ app: prometheus-adapter
+ name: prometheus-adapter
+ spec:
+ serviceAccountName: custom-metrics-apiserver
+ containers:
+ - name: prometheus-adapter
+ image: directxman12/k8s-prometheus-adapter-amd64
+ args:
+ - --secure-port=6443
+ - --tls-cert-file=/var/run/serving-cert/tls.crt
+ - --tls-private-key-file=/var/run/serving-cert/tls.key
+ - --logtostderr=true
+ - --prometheus-url=http://prometheus-operated:9090/
+ - --metrics-relist-interval=1m
+ - --v=4
+ - --config=/etc/adapter/config.yaml
+ ports:
+ - containerPort: 6443
+ volumeMounts:
+ - mountPath: /var/run/serving-cert
+ name: volume-serving-cert
+ readOnly: true
+ - mountPath: /etc/adapter/
+ name: config
+ readOnly: true
+ - mountPath: /tmp
+ name: tmp-vol
+ volumes:
+ - name: volume-serving-cert
+ secret:
+ secretName: prometheus-adapter-tls
+ - name: config
+ configMap:
+ name: adapter-config
+ - name: tmp-vol
+ emptyDir: {}
+
RBAC의 정의는 Adopter가 동작하는데 필요한 ServiceAccount
, ClusterRole
, RoleBinding
, ClusterRoleBinding
을 작성합니다.
Prometheus 구성이 배포된 prometheus-metric
프로젝트에서 수행
좌측 Overview 클릭 후 목록에서 prometheus-example
의 route 클릭하여 Prometheus Console 확인
Execute
우측의 -insert metric at cursor-
목록에서 추가된 jboss_active_requests
항목 선택 후 Execute
클릭 하여 값 확인
Element | Value |
---|---|
jboss_active_requests | 0 |
jmx_exporter_conf.yaml
에서 부여 한 값Adopter가 애플리케이션 Metric을 검색할 수 있는지 확인하여 정상적으로 동작하고 있는지 확인이 필요합니다.
Adopter가 애플리케이션 Metric 정보를 검색하도록 구성되었는지 확인
$ oc get --raw "/apis/custom.metrics.k8s.io/v1beta1" | jq .
+{
+ "kind": "APIResourceList",
+ "apiVersion": "v1",
+ "groupVersion": "custom.metrics.k8s.io/v1beta1",
+ "resources": [
+ {
+ "name": "namespaces/jboss_active_avg",
+ "singularName": "",
+ "namespaced": false,
+ "kind": "MetricValueList",
+ "verbs": [
+ "get"
+ ]
+ },
+ {
+ "name": "pods/jboss_active_avg",
+ "singularName": "",
+ "namespaced": true,
+ "kind": "MetricValueList",
+ "verbs": [
+ "get"
+ ]
+ },
+ {
+ "name": "services/jboss_active_avg",
+ "singularName": "",
+ "namespaced": true,
+ "kind": "MetricValueList",
+ "verbs": [
+ "get"
+ ]
+ }
+ ]
+}
+
애플리케이션 Metric인 jboss_active_avg
가 검색되는 지 확인
$ oc get --raw "/apis/custom.metrics.k8s.io/v1beta1/namespaces/my-eap-project/metrics/jboss_active_avg" | jq .
+{
+ "kind": "MetricValueList",
+ "apiVersion": "custom.metrics.k8s.io/v1beta1",
+ "metadata": {
+ "selfLink": "/apis/custom.metrics.k8s.io/v1beta1/namespaces/my-eap-project/metrics/jboss_active_avg"
+ },
+ "items": [
+ {
+ "describedObject": {
+ "kind": "Namespace",
+ "name": "my-eap-project",
+ "apiVersion": "/v1"
+ },
+ "metricName": "jboss_active_avg",
+ "timestamp": "2020-01-30T07:12:01Z",
+ "value": "3",
+ "selector": null
+ }
+ ]
+}
+
결과 값은 JBoss 상의 Active Request가 3개임을 의미 합니다.
Horizontal Pod Autoscaler (HPA) 리소스를 적용합니다.
$ oc apply -f - <<EOF
+kind: HorizontalPodAutoscaler
+apiVersion: autoscaling/v2beta1
+metadata:
+ name: sample-eap
+ namespace: my-eap-project
+spec:
+ scaleTargetRef:
+ apiVersion: apps.openshift.io/v1
+ kind: DeploymentConfig
+ name: sample-eap
+ minReplicas: 1
+ maxReplicas: 5
+ metrics:
+ - type: Object
+ object:
+ target:
+ kind: Namespace
+ name: my-eap-project
+ metricName: jboss_active_avg
+ targetValue: 10
+EOF
+
namespace
를 기준으로 쿼리하기 때문에 Object
형태의 metrics를 구성합니다.
적용된 HPA를 확인하여 적용된 JBoss EAP에 요청에 따라 값이 변화하고 Pod의 수가 변화하는지 확인합니다.
$ watch oc describe hpa/sample-eap -n my-eap-project
+Every 2.0s: oc describe hpa/sample-eap -n my-eap-pr... Thu Jan 30 07:23:47 2020
+
+Name: sample-eap
+Namespace: my-eap-project
+Labels: <none>
+Annotations: kubectl.kubernetes.io/last-ap
+plied-configuration={"apiVersion":"autoscaling/v2beta1","kind":"HorizontalPodAut
+oscaler","metadata":{"annotations":{},"name":"sample-eap","namespace":"my-eap-pr
+oject"},"sp...
+CreationTimestamp: Thu, 30 Jan 2020 07:18:17 +00
+00
+Reference: DeploymentConfig/sample-eap
+Metrics: ( current / target )
+ "jboss_active_avg" on Namespace/my-eap-project: 16 / 10
+Min replicas: 1
+Max replicas: 5
+DeploymentConfig pods: 1 current / 2 desired
+Conditions:
+ Type Status Reason Message
+ ---- ------ ------ -------
+ AbleToScale True SucceededRescale the HPA controller was able to upd
+ate the target scale to 2
+ ScalingActive True ValidMetricFound the HPA was able to successfully c
+alculate a replica count from Namespace metric jboss_active_avg
+ ScalingLimited False DesiredWithinRange the desired count is within the ac
+ceptable range
+Events:
+ Type Reason Age From Message
+ ---- ------ ---- ---- -------
+ Normal SuccessfulRescale 17s horizontal-pod-autoscaler New size: 2; reaso
+n: Namespace metric jboss_active_avg above target
+
다음의 정보가 도움이 됩니다. :
v2beta1 api를 적용하는 방법은 다음과 같습니다.
master 노드의 master-config.yaml
수정
$ vi /etc/origin/master/master-config.yaml
+
apiServerArguments
에 runtime-config
항목으로 apis/autoscaling/v2beta1=true
추가
kubernetesMasterConfig:
+...
+ apiServerArguments:
+ ...
+ runtime-config:
+ - apis/autoscaling/v2beta1=true
+ controllerArguments:
+...
+
master 구성요소 재시작
$ master-restart api
+$ master-restart controllers
+$ systemctl restart atomic-openshift-node.service
+
api 응답 확인
$ oc get --raw /apis/autoscaling/v2beta1
+{"kind":"APIResourceList","apiVersion":"v1","groupVersion":"autoscaling/v2beta1","resources":[{"name":"horizontalpodautoscalers","singularName":"","namespaced":true,"kind":"HorizontalPodAutoscaler","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["hpa"],"categories":["all"]},{"name":"horizontalpodautoscalers/status","singularName":"","namespaced":true,"kind":"HorizontalPodAutoscaler","verbs":["get","patch","update"]}]}
+
HPA 적용 :
예를들어 mnist-app
이라고 하는 DeploymentConfig
를 타겟으로 함
apiVersion: autoscaling/v2beta1
+kind: HorizontalPodAutoscaler
+metadata:
+ name: mnist-app
+ namespace: my-namespace
+spec:
+ maxReplicas: 10
+ minReplicas: 1
+ scaleTargetRef:
+ apiVersion: apps.openshift.io/v1
+ kind: DeploymentConfig
+ name: mnist-app
+ metrics:
+ - type: Resource
+ resource:
+ name: memory
+ targetAverageUtilization: 10
+ - type: Resource
+ resource:
+ name: memory
+ targetAverageValue: 1G
+
Red Hat Container Image Registry에 접속하기 위한 전용 계정을 생성할 수 있습니다.
Service Accounts
를 클릭New Service Account
버튼을 클릭CREATE
버튼 클릭Token Information
탭의 정보를 확인생성한 Token으로 본래의 계정 접속 정보를 노출 하지 않고 Red Hat Container Image Registry에 접속할 수 있는 접속 정보로 사용할 수 있습니다.
OpenShift에 리소스를 배포하는 방법은 oc
CLI를 사용하는 방법과 Application Console을 활용하는 두가지 방법이 있습니다.
다음과 같은 prometheus-operator
관련 리소스 설정이 있다고 가정합니다.
apiVersion: operators.coreos.com/v1alpha1
+kind: Subscription
+metadata:
+ generateName: prometheus-
+ namespace: custom-metric
+spec:
+ source: rh-operators
+ name: prometheus
+ startingCSV: prometheusoperator.0.22.2
+ channel: preview
+
yaml로 작성된 리소스 설정 파일을 적용하기위해 다음 두가지 형태를 활용합니다.
oc
로그인 상태에서 yaml로 작성된 리소스 설정을 파일로 저장합니다. 예를 들어 prometheus-operator.yaml
인 경우 다음과 같이 적용 가능합니다.
$ oc apply -f prometheus-operator.yaml
+
또는 다음과 같이 인라인으로 수행할 수 있습니다.
$ $ oc create -f - <<EOF
+apiVersion: operators.coreos.com/v1alpha1
+kind: Subscription
+metadata:
+ generateName: prometheus-
+ namespace: custom-metric
+spec:
+ source: rh-operators
+ name: prometheus
+ startingCSV: prometheusoperator.0.22.2
+ channel: preview
+EOF
+
OpenShift Application Console 에 접속 합니다. 적용할 프로젝트를 클릭하고 Overview
화면의 우측 상단에 Add to Project
를 클릭합니다.Import YAML/JSON
을 클릭하여 리소스 설정을 입력받는 창에 앞서의 예로 작성된 설정을 붙여넣습니다. 또는 파일로 저장된 파일을 선택하여 업로드/적용 가능합니다. Create
버튼을 클릭하여 결과를 확인합니다.
#수정 할 파일
+vi /etc/systemd/system/vmtoolsd.service
+
+[Unit]
+Description=Service for virtual machines hosted on VMware
+Documentation=http://open-vm-tools.sourceforge.net/about.php
+ConditionVirtualization=vmware
+DefaultDependencies=no
+Before=cloud-init-local.service
+#아래 After=dbus.service추가
+After=dbus.service
+After=vgauth.service
+After=apparmor.service
+RequiresMountsFor=/tmp
+After=systemd-remount-fs.service systemd-tmpfiles-setup.service systemd-modules-load.service
+
+[Service]
+ExecStart=/usr/bin/vmtoolsd
+TimeoutStopSec=5
+
+[Install]
+WantedBy=multi-user.target
+Alias=vmtoolsd.service
+
+
brew install aliyun-cli
+
인증 메커니즘이 4가지이며 구성 설정시 --mode
에 Credential type을 넣어서 구성하게 됨
Credential type | Limit | Interactive credential configuration (fast) | Non-interactive credential configuration |
---|---|---|---|
AK | Use an AccessKey ID and an AccessKey secret to authorize access. | Configure AccessKey credential | Configure AccessKey credential |
StsToken | Use a Security Token Service (STS) token to authorize access. | Configure STS token credential | Configure STS token credential |
RamRoleArn | Use the role of a Resource Access Management (RAM) user to authorize access. | Configure RamRoleArn credential | Configure RamRoleArn credential |
EcsRamRole | Use the RAM role of an Elastic Compute Service (ECS) instance to authorize password-free access. | Configure EcsRamRole credential | Configure EcsRamRole credential |
AccessKey 방식의 인증 정보가 없는 경우 아래와 같이 생성
Accesskey Management
클릭AccessKey Pair
항목에서 Create AccessKey Pair
를 클릭하여 AK(AccessKey)를 신규로 생성aliyun cli의 configure
를 실행
--mode
에는 Credential Type을 지정--profile
에는 사용할 이름을 사용자가 지정, default
는 기본 값$ aliyun configure --mode AK --profile myprofile
+Configuring profile 'myprofile' in 'AK' authenticate mode...
+Access Key Id []: LTAI5**********************t88V
+Access Key Secret []: *******************************
+Default Region Id []: ap-southeast-1
+Default Output Format [json]: json (Only support json)
+Default Language [zh|en] en: en
+Saving profile[gslee] ...Done.
+
+Configure Done!!!
+
구성 완료 후 home 디렉토리의 .aliyun
디렉토리 내의 config.json
에서 구성된 정보를 확인 가능
{
+ "current": "myprofile",
+ "profiles": [
+ {
+ "name": "default",
+ },
+ {
+ "name": "gslee",
+ "mode": "AK",
+ "access_key_id": "LTAI5**********************t88V",
+ "region_id": "ap-southeast-1",
+ "output_format": "json",
+ "language": "en",
+ }
+ ],
+ "meta_path": ""
+}
+
Region 목록 확인
팁
profile이 default
인 경우 --profile or -p
생략 가능
$ aliyun -p myprofile ecs DescribeRegions
+{
+ "Regions": {
+ "Region": [
+ {
+ "LocalName": "华北1(青岛)",
+ "RegionEndpoint": "ecs.aliyuncs.com",
+ "RegionId": "cn-qingdao"
+ },
+ {
+ "LocalName": "华北2(北京)",
+ "RegionEndpoint": "ecs.aliyuncs.com",
+ "RegionId": "cn-beijing"
+ },
+ ]
+ },
+ "RequestId": "2304DF19-CABF-54DF-BDC6-F889C3A73E4F"
+}
+
이 과정은 IaC 도구인 Terraform을 사용하여 클라우드 리소스를 생성하는 실습(Hands-on)과정입니다.
💻 표시는 실제 실습을 수행하는 단계 입니다.
사전 준비 사항
컨텐츠
plan
apply
destroy
서로의 기술적 백그라운드를 이해하고 기술배경에 맞춰 워크샵이 진행됩니다.
Hashicorp Terraform 적용 단계 영상(자막)
새로운 NCP의 인스턴스를 프로비저닝 할 수있는 몇 가지 다른 방법을 살펴 보겠습니다. 시작하기 전에 다음을 포함한 몇 가지 기본 정보를 수집해야합니다 (더 많은 옵션이 있습니다).
서버 생성을 위한 CLI 가이드 : https://cli.ncloud-docs.com/docs/cli-server-createserverinstances
ncloud server createServerInstances \
+ --serverImageProductCode SPSW0LINUX000046 \
+ --serverProductCode SPSVRSTAND000003 \
+ --serverName ncloud-mktest
+
파라미터 명 | 필수 여부 | 타입 | 제약사항 |
---|---|---|---|
serverImageProductCode | Conditional | String | Min:1, Max:20 |
serverProductCode | No | String | Min:1, Max:20 |
memberServerImageNo | Conditional | String | |
serverName | No | String | Min:3, Max:30 |
serverDescription | No | String | Min:1, Max:1000 |
loginKeyName | No | String | Min:3, Max:30 |
isProtectServerTermination | No | Boolean | |
serverCreateCount | No | Integer | Min:1, Max:20 |
serverCreateStartNo | No | Integer | |
internetLineTypeCode | No | String | Min:1, Max:5 |
feeSystemTypeCode | No | String | Min:1, Max:5 |
zoneNo | No | String | |
accessControlGroupConfigurationNoList | No | List | Min:0, Max:5 |
raidTypeName | Conditional | String | |
userData | No | String | Min:1, Max:21847 |
initScriptNo | No | String | |
instanceTagList.tagKey | No | String | |
instanceTagList.tagValue | No | String | |
isVaccineInstall | No | Boolean | |
blockDevicePartitionList.N.mountPoint | No | String | "/" (root) 경로로 시작하는 마운트 포인트를 입력합니다. 첫 번째 마운트 포인트는 반드시 "/" (root) 파티션이어야 합니다. "/" (root) 하위 명칭은 소문자와 숫자만 허용되며, 소문자로 시작해야합니다. OS 종류에 따라서 /root, /bin, /dev 등의 특정 키워드는 사용 불가능 할 수 있습니다. |
blockDevicePartitionList.N.partitionSize | No | String | Min : 50 GiB |
nCloud CLI는 자동화할 수 있는 스크립트 방식을 제공합니다. 하지만 이 작업을 실행하기 전에 예측할 수 있나요?
nCloud 서버를 생성하는 Terraform 예제 코드 : https://registry.terraform.io/providers/NaverCloudPlatform/ncloud/latest/docs/resources/server
resource "ncloud_server" "server" {
+ name = "tf-test-vm1"
+ server_image_product_code = "SPSW0LINUX000032"
+ server_product_code = "SPSVRSTAND000004"
+
+ tag_list {
+ tag_key = "samplekey1"
+ tag_value = "samplevalue1"
+ }
+
+ tag_list {
+ tag_key = "samplekey2"
+ tag_value = "samplevalue2"
+ }
+}
+
resource "ncloud_server" "server" {
+ name = "tf-test-vm1"
+ server_image_product_code = "SPSW0LINUX000032"
+ server_product_code = "SPSVRSTAND000004"
+}
+
IaC (Infrastructure as Code)는 컴퓨터에서 읽을 수있는 정의 파일을 사용하여 클라우드 인프라를 관리하고 프로비저닝하는 프로세스입니다.
실행 가능한 '문서'라고 생각하시면 됩니다.
JSON:
"name": "{ "Fn::Join" : [ "-", [ PilotServerName, vm ] ] }",
+
Terraform:
name = "${var.PilotServerName}-vm"
+
Terraform 코드 (HCL)는 배우기 쉽고 읽기 쉽습니다. 또한 동등한 JSON 구성보다 50-70 % 더 간결합니다.
Terraform은 오픈 소스 프로비저닝 도구입니다.
Go로 작성된 단일 바이너리로 제공됩니다. Terraform은 크로스 플랫폼이며 Linux, Windows 또는 MacOS에서 실행할 수 있습니다.
terraform 설치는 쉽습니다. zip 파일을 다운로드하고 압축을 풀고 실행하기 만하면됩니다.
기본적으로 Terraform 오픈소스는 커맨드라인 도구입니다.
Terraform 명령은 수동으로 입력하거나 스크립트에서 자동으로 실행됩니다.
명령은 Linux, Windows 또는 MacOS에 상관없이 동일합니다.
Terraform에는 다른 작업을 수행하는 하위 명령들이 있습니다.
# Basic Terraform Commands
+terraform version
+terraform help
+terraform init
+terraform plan
+terraform apply
+terraform destroy
+
$ terraform help
+Usage: terraform [-version] [-help] <command> [args]
+...
+Common commands:
+ apply Builds or changes infrastructure
+ console Interactive console for Terraform interpolations
+ destroy Destroy Terraform-managed infrastructure
+ env Workspace management
+ fmt Rewrites config files to canonical format
+ graph Create a visual graph of Terraform resources
+
특정 하위 명령에 대한 도움말을 보려면 terraform <subcommand> help
를 입력합니다.
resource "ncloud_vpc" "vpc" {
+ ipv4_cidr_block = "10.0.0.0/16"
+}
+
Terraform 코드는 HCL2 툴킷을 기반으로합니다. HCL은 HashiCorp Configuration Language를 나타냅니다.
Terraform 코드는 모든 클라우드 또는 플랫폼에서 인프라를 프로비저닝하기 위해 특별히 설계된 선언적 언어입니다.
줄 주석은 *
(octothorpe, 별표) 또는 #
(파운드) 기호로 시작합니다....샵! #
# This is a line comment.
+
블록 주석은 /*
와 */
기호 사이에 포함됩니다.
/* This is a block comment.
+Block comments can span multiple lines.
+The comment ends with this symbol: */
+
Workspace는 Terraform 코드가 포함 된 폴더 또는 디렉토리입니다.
Terraform 파일은 항상* .tf 또는* .tfvars 확장자로 끝납니다. 실행 시 해당 파일들은 하나로 동작합니다.
대부분의 Terraform Workspaces에는 일반적으로 아래 3개정도의 파일을 둡니다. (정해진건 아닙니다.)
$ terraform init
+Initializing the backend...
+
+Initializing provider plugins...
+- Finding navercloudplatform/ncloud versions matching ">= 2.1.2"...
+- Installing navercloudplatform/ncloud v2.1.2...
+- Installed navercloudplatform/ncloud v2.1.2 (signed by a HashiCorp partner, key ID 9DCE24305722E9C9)
+...
+Terraform has been successfully initialized!
+
Terraform은 필요한 Provider(공급자)와 Module(모듈)을 가져와 .terraform
디렉터리에 저장합니다. 모듈 또는 공급자를 추가, 변경 또는 업데이트하는 경우 init를 다시 실행해야합니다.
$ terraform plan
+...
+Terraform will perform the following actions:
+
+ # ncloud_vpc.vpc will be created
+ + resource "ncloud_vpc" "vpc" {
+ + default_access_control_group_no = (known after apply)
+ + default_network_acl_no = (known after apply)
+ + default_private_route_table_no = (known after apply)
+ + default_public_route_table_no = (known after apply)
+ + id = (known after apply)
+ + ipv4_cidr_block = "10.0.0.0/16"
+ + name = (known after apply)
+ + vpc_no = (known after apply)
+ }
+
+Plan: 1 to add, 0 to change, 0 to destroy.
+
변경 사항을 적용하기 전에 terraform plan
으로 미리 구성의 변경을 살펴봅니다.
Terraform 변수는 variables.tf
라는 파일에 일반적으로 위치 시킵니다.(이름은 변경 가능) 변수는 기본 설정을 가질 수 있습니다. 기본값을 생략하면 사용자에게 값을 입력하라는 메시지가 표시됩니다. 여기서 우리는 사용하려는 변수를 선언 합니다.
variable "prefix" {
+ description = "This prefix will be included in the name of most resources."
+}
+
+variable "vpc_cidr" {
+ description = "A cidr option for instances into the VPC."
+ default = "10.0.0.0/16"
+}
+
일부 변수를 정의한 후에는 다른 방법으로 설정하고 재정의 할 수 있습니다. 다음은 각 방법의 우선 순위입니다.
이 목록은 가장 높은 우선 순위 (1)에서 가장 낮은 순위 (5)로 나타냅니다.
즉, CLI 실행시 -var
로 지정되는 Command line flag
가 가장 우선합니다.
실습을 위해 다음장으로 이동하세요.
@slidestart blood
@slideend
테라폼 다운로드 사이트 https://www.terraform.io/downloads.html 로 접속하여, 자신의 환경에 맞는 Terraform을 다운로드 받습니다.
압축을 해제하여 terraform 바이너리 파일을 확인합니다.
terraform
terraform.exe
mkdir ~/hashicorp/bin
+mv terraform ~/hashicorp/bin
+cd ~/hashicorp/bin
+echo $(pwd) >> ~/.bash_profile
+source ~/.bash_profile
+
mkdir ~/hashicorp/bin
+mv terraform ~/hashicorp/bin
+cd ~/hashicorp/bin
+echo $(pwd) >> ~/.zshrc
+source ~/.zshrc
+
VSCode 편집기를 사용할 준비가 되었다면, 코드의 시인성을 위해 extension을 설치 합니다.
Terraform
을 검색합니다.HashiCorp Terraform
을 설치합니다.이 실습에서는 Terraform을 실행하기 위한 IDE 설정과 Terraform CLI를 사용하고, NCP를 위한 기본 구성을 수행합니다.
실습에서 사용할 코드는 github에서 받습니다.
링크 : https://github.com/ncp-hc/workshop-oss
git 이 설치되어있는 경우 git clone
을 통해 코드를 받습니다.$ git clone https://github.com/ncp-hc/workshop-oss.git
Download만을 원하는 경우 아래 Download ZIP
을 선택합니다.
Open Folder...
를 클릭합니다.lab01
을 열어줍니다.@slidestart blood
@slideend
편집기가 준비가 되었으면 터미널을 열고 몇가지 기본적인 Terraform 명령을 수행합니다.
Linux/Mac의 경우 터미널
에서 수행하거나 Windows의 경우 명령 프롬프트
에서 실행하게 됩니다.
VSCode 편집기를 사용하게 되면, 편집기의 터미널(Terminal) 기능으로 함께 사용할 수 있습니다.
terraform version
+
terraform help
+
@slidestart blood
@slideend
NCP에 인증하고 리소스를 빌드하기 위해 Terraform은 적절한 자격 증명 세트를 제공하도록 요구합니다.
인증키 관리
를 선택합니다.신규 API 인증키 생성
버튼을 클릭합니다.API 인증키가 생성 되었습니다.
라는 메시지를 확인합니다.Secret Key
항목의 보기
버튼을 클릭하여 Secret Key
를 확인합니다.Access Key ID
, Secret Key
를 사용하게 됩니다.이 교육 환경을 위해 NCP의 자격 증명을 준비하여 환경 변수로 저장합니다. Terraform은 쉘 환경에 구성된 환경 변수를 자동으로 읽고 사용합니다.
NCLOUD_ACCESS_KEY
NCLOUD_SECRET_KEY
NCLOUD_REGION
를 환경변수로 등록합니다.export NCLOUD_ACCESS_KEY="XXXXXXXXXXXXX"
+export NCLOUD_SECRET_KEY="XXXXXXXXXXXXXXXXXXXXXXXXXX"
+
set NCLOUD_ACCESS_KEY="XXXXXXXXXXXXX"
+set NCLOUD_SECRET_KEY="XXXXXXXXXXXXXXXXXXXXXXXXXX"
+
$Env:NCLOUD_ACCESS_KEY="XXXXXXXXXXXXX"
+$Env:NCLOUD_SECRET_KEY="XXXXXXXXXXXXXXXXXXXXXXXXXX"
+
위험
API 자격증명정보는 실수로 공개된 저장소에 노출되거나 복사되면 위헐합니다.
자격증명(API 인증키)를 코드에 저장하지 않는 것을 권장합니다.
@slidestart blood
*.tf
또는 *.tfvars
로 끝나는 모든 것을 읽습니다.main.tf
, variables.tf
, outputs.tf
파일로 구성됩니다.예를 들어 모든 로드 밸런서 구성 코드를 load_balancer.tf
에 구성하기
@slideend
코드 편집기의 파일 목록이 보이십니까?
Terraform 코드는 항상 .tf
확장자로 끝납니다. 원하는 만큼 Terraform 파일을 가질 수 있지만 일반적으로 다음 세 가지를 구성합니다.
main.tf
- 대부분의 Terraform 코드가 저장되는 위치입니다. 이것은 자원을 구축하는 부분입니다.variables.tf
- 이 파일을 사용하여 사용자가 사용할 수 있는 변수를 정의합니다.output.tf
- 이 파일에는 성공적인 Terraform 실행이 끝날 때 표시될 출력이 포함되어 있습니다.Terraform에서 *.tf
와 *.tfvars
로 끝나지 않는 파일은 무시됩니다.
@slidestart blood
https://registry.terraform.io/browse/providers
ncloud
provider 입니다.@slideend
우리는 이 실습에서 사용할 Terraform 코드를 다운로드 했습니다. 나머지 실습에서 이 소스코드를 사용할 것입니다.
Terraform으로 무엇이든 하기 전에 Workspace를 초기화 해야 합니다.
init
명령을 수행합니다.terraform init
+
+...
+Terraform has been successfully initialized!
+
`terraform init 명령은 Terraform 코드를 스캔하고 필요한 Provider를 식별하고 다운로드합니다.
.terraform
디렉토리에 설치되었는지 확인합니다.ls .terraform/providers/registry.terraform.io/navercloudplatform
+
dir .terraform/providers/registry.terraform.io/navercloudplatform
+
이 숨겨진 디렉토리는 모든 모듈과 플러그인이 저장되는 곳입니다.
Q. Terraform은 모듈과 공급자를 어디에 저장합니까?
@slidestart blood
terraform validate
명령으로 실행할 수 있습니다.@slideend
Terraform에는 validate
라는 하위 명령이 내장되어 있습니다. 이것은 코드가 올바르게 구문 분석되는지 확인하기 위해 코드의 빠른 구문 검사를 수행하려는 경우에 유용합니다.
main.tf
16번째 행의 사이에 큰 따옴표를 제거 ncloud_vpc
와 hashicat
사이의 큰 따옴표를 제거하고 저장합니다.
terraform {
+ required_providers {
+ ncloud = {
+ source = "NaverCloudPlatform/ncloud"
+ version = ">= 2.1.2"
+ }
+ }
+}
+
+provider "ncloud" {
+ region = var.region
+ site = var.site
+ support_vpc = true
+}
+
+resource "ncloud_vpc" "hashicat" { → resource "ncloud_vpc" hashicat" {
+ ipv4_cidr_block = "10.0.0.0/16"
+ name = lower("${var.prefix}-vpc-${var.region}")
+}
+
+...생략...
+
terraform validate
명령을 터미널에서 실행합니다.
terraform validate
+
다시 따옴표 표기를 넣고 저장한 다음 terraform validate
명령을 실행합니다. 이번에는 검증을 통과해야 합니다.
terraform validate
명령은 자동화된 CI/CD 테스트 파이프라인에서 가장 자주 사용됩니다. 다른 단계를 수행하기 전에 코드에서 오류를 빠르게 포착할 수 있습니다.
@slidestart blood
terraform plan
을 통해 환경에 대한 변경 사항을 안전한 방법으로 미리 볼 수 있습니다.@slideend
terraform plan
명령을 실행합니다.$ terraform plan
+var.prefix
+ This prefix will be included in the name of most resources.
+
+ Enter a value:
+
이 명령을 실행하면 Terraform에서 prefix
변수 를 입력하라는 메시지를 표시 합니다.
소문자 또는 숫자의 짧은 문자열을 입력합니다. 영문 이니셜 소문자를 사용하는 것이 좋습니다.
prefix
는 현재 Terraform 코드 구성에서 VPC, 서브넷, 서버 등의 리소스 이름의 일부가 됩니다.
@slidestart blood
terraform.tfvars
파일은 사용자가 변수를 구성할 수 있는 편리한 위치입니다.@slideend
Terraform에서 모든 변수는 사용하기 전에 선언되어야 합니다. 변수는 다른 *.tf
파일에서도 선언될 수 있지만 일반적으로 variables.tf
파일에서 선언됩니다. (default)
variable "address_space" {
+ description = "The address space that is used by the virtual network. You can supply more than one address space. Changing this forces a new resource to be created."
+ default = "10.0.0.0/8"
+}
+
해당 값은 terraform.tfvars
파일 및 나중에 다른 방법으로 설정할 수 있습니다.
terraform.tfvars
파일을 수정합니다.terraform.tfvars
파일을 열고 prefix
의 줄 시작 부분에 주석 기호 #
를 제거합니다.
yourname
을 원하는 소문자 또는 숫자의 짧은 문자열을 입력합니다.
# terraform.tfvars
+prefix = "yourname"
+
이제 terraform plan
을 다시 실행 합니다. 이번에는 prefix
를 수동으로 입력할 필요가 없습니다.
@slidestart blood
terraform.tfvars
파일에 설정하여 variables.tf
파일에 정의된 모든 변수를 재정의할 수 있습니다.@slideend
이전 실습에서 prefix
를 terraform.tfvars
파일에서 변수를 설정했습니다. ncloud 인프라가 배포될 vpc의 cidr를 결정할 또 다른 변수를 설정해 보겠습니다.
먼저 다른 계획을 실행하여 위치를 변경한 후 어떻게 되는지 비교할 수 있습니다.
terraform plan
+
address_space
정보를 수정합니다.default로 선언되어있는 값 외에 사용자 지정 변수로 변경해봅니다. terraform.tfvars
파일을 열어 address_space
을 추가하고 다시 terraform plan
을 실행해 봅니다. 이번엔 무엇이 다른가요?
# terraform.tfvars
+prefix = "yourname"
+address_space = "10.0.0.0/16"
+
terraform.tfvars
파일은 variables.tf
파일에 선언된 모든 변수에 대한 값을 설정할 수 있음을 기억하십시오.
Q. Terraform 변수는 일반적으로 어디에 선언 됩니까?
이 장에서 우리는 :
모든 Terraform으로 구성되는 리소스는 정확히 동일한 방식으로 구성됩니다.
resource type "name" {
+ parameter = "foo"
+ parameter2 = "bar"
+ list = ["one", "two", "three"]
+}
+
ncloud_vpc
Terraform Core는 무엇이든 빌드하려면 하나 이상의 Provider가 필요합니다.
사용하려는 Provider의 버전을 쑤동으로 구성 할 수 있습니다. 이 옵션을 비워두면 Terraform은 기본적으로 사용 가능한 최신 버전의 Provider를 사용합니다.
terraform {
+ required_providers {
+ ncloud = {
+ source = "NaverCloudPlatform/ncloud"
+ version = ">= 2.1.2"
+ }
+ }
+}
+
+provider "ncloud" { }
+
$ terraform apply
+...
+Terraform will perform the following actions:
+
+ # ncloud_vpc.hashicat will be created
+ + resource "ncloud_vpc" "hashicat" {
+ + default_access_control_group_no = (known after apply)
+ ...
+ + ipv4_cidr_block = "10.0.0.0/16"
+ + vpc_no = (known after apply)
+ }
+
+Plan: 1 to add, 0 to change, 0 to destroy.
+
+Do you want to perform these actions?
+ Terraform will perform the actions described above.
+ Only 'yes' will be accepted to approve.
+
+ Enter a value:
+
terraform apply
는 우선 plan
을 실행하고, 승인하면 변경 사항을 적용합니다.
$ terraform apply
+...
+Terraform will perform the following actions:
+
+ # ncloud_vpc.hashicat will be destoryed
+ - resource "ncloud_vpc" "hashicat" {
+ ...
+ - ipv4_cidr_block = "10.0.0.0/16" -> null
+ }
+
+Plan: 0 to add, 0 to change, 1 to destroy.
+
+Do you want to perform these actions?
+ Terraform will perform the actions described above.
+ Only 'yes' will be accepted to approve.
+
+ Enter a value:
+
terraform destroy
는 action
과 반대 입니다. 승인하면 인프라가 제거됩니다.
Terraform은 내장 된 코드 포맷터/클리너와 함께 제공됩니다. 모든 여백과 목록 들여 쓰기를 깔끔하고 깔끔하게 만들 수 있습니다. 아름다운 코드가 더 잘 동작하는 것(?) 같습니다.
terraform fmt
+
*.tf
파일이 포함 된 디렉토리에서 실행하기 만하면 코드가 정리됩니다.
data "ncloud_member_server_images" "prod" {
+ filter {
+ name = "name"
+ values = [data.terraform_remote_state.image_name.outputs.image_name]
+ }
+}
+
+resource "ncloud_server" "server" {
+ name = "${var.server_name}${random_id.id.hex}"
+ member_server_image_no = data.ncloud_member_server_images.prod.member_server_images.0
+ server_product_code = "SPSVRGPUSSD00001"
+ login_key_name = ncloud_login_key.key.key_name
+ zone = var.zone
+}
+
Data Source(data)는 Provider가 기존 리소스를 반환하도록 쿼리하는 방법입니다.
생성되어있는 리소스나 Provider로 조회할 수 있는 리소스 정보를 다른 리소스 구성에서 접근할 수 있습니다.
data "ncloud_member_server_images" "prod" {
+ filter {
+ name = "name"
+ values = [data.terraform_remote_state.image_name.outputs.image_name]
+ }
+}
+
+resource "ncloud_server" "server" {
+ name = "${var.server_name}${random_id.id.hex}"
+ member_server_image_no = data.ncloud_member_server_images.prod.member_server_images.0
+ server_product_code = "SPSVRGPUSSD00001"
+ login_key_name = ncloud_login_key.key.key_name
+ zone = var.zone
+}
+
Terraform은 자동으로 종속성을 추적 할 수 있습니다. 앞서 설명된 리소스를 살펴보십시오. ncloud_server 리소스에서 강조 표시된 줄을 확인합니다. 이것이 테라 폼에서 한 리소스가 다른 리소스를 참조하도록하는 방법입니다.
Terraform은 Workspace에서 .tf
확장자로 끝나는 모든 파일을 읽지만 대표적으로는 main.tf
, variables.tf
, outputs.tf
를 갖는 것입니다. 원하는 경우 더 많은 tf 파일을 추가 할 수 있습니다.
파일 구조
Workspace
+├── `main.tf`
+├── `outputs.tf`
+├── terraform.tfvars
+└── `variables.tf`
+
이러한 각 파일을 자세히 살펴 보겠습니다.
main.tf
파일첫 번째 파일은 main.tf
입니다. 일반적으로 테라 폼 코드를 저장하는 곳입니다. 더 크고 복잡한 인프라를 사용하면이를 여러 파일로 나눌 수 있습니다.
resource "ncloud_vpc" "main" {
+ ipv4_cidr_block = var.address_space
+ name = lower("${var.prefix}-vpc-${var.region}")
+}
+
+resource "ncloud_subnet" "main" {
+ name = "${var.name_scn02}-public"
+ vpc_no = ncloud_vpc.vpc_scn_02.id
+ subnet = cidrsubnet(ncloud_vpc.main.ipv4_cidr_block, 8, 0)
+ zone = "KR-2"
+ network_acl_no = ncloud_network_acl.network_acl_02_public.id
+ subnet_type = "PUBLIC"
+}
+
+...생략...
+
variable.tf
파일두 번째 파일은 variables.tf
입니다. 여기에서 변수를 정의하고 선택적으로 일부 기본값을 설정합니다.
variable "prefix" {
+ description = "This prefix will be included in the name of most resources."
+}
+
+variable "region" {
+ description = "The region where the resources are created."
+ default = "KR"
+}
+
output.tf
파일output.tf
파일은 테라 폼 적용이 끝날 때 표시 할 메시지 또는 데이터를 구성하는 곳입니다.
output "acl_public_id" {
+ value = ncloud_network_acl.network_acl_public.id
+}
+
+output "public_addr" {
+ value = "http://${ncloud_public_ip.main.public_ip}:8080"
+}
+
terraform 리소스 그래프는 리소스 간의 종속성을 시각적으로 보여줍니다.
Region
및 Prefix
변수는 리소스 그룹을 만드는 데 필요하며 이는 가상 네트워크를 구축하는 데 필요합니다.
실습을 위해 다음장으로 이동하세요.
Open Folder...
를 클릭합니다.lab02
을 열어줍니다.@slidestart blood
@slideend
💻 다음 terraform graph명령을 실행해 보세요.
새로운 Workspace 이므로, terraform init
을 수행합니다.
terraform init
+
terraform graph
를 수행합니다.
terraform graph
+
그러면 digraph
로 시작하는 인프라의 시각적 맵을 만드는 데 사용할 수 있는 코드가 생성됩니다. 그래프 데이터는 DOT 그래프 설명 언어 형식 입니다. 무료 Blast Radius 도구를 포함하여 이 데이터를 시각화하는 데 사용할 수 있는 몇 가지 그래프 도구가 있습니다.
digraph
로 시작하는 내용을 복사하여 붙여넣고 어떤 그림이 나오는지 확인해 봅니다.온라인에 Terraform의 작업을 시각화해주는 여러가지 툴이 있습니다. 간혹 plan 파일을 요구하는 툴이 있다면 주의하십시오. 민감한 정보가 포한된 plan의 경우 보안적으로 위험할 수 있습니다. 주의하여 사용하세요.
경고
plan 정보에는 인증키, 패스워드같은 노출하고 싶지 않은 정보가 포함될 수 있습니다.
@slidestart blood
terraform apply
명령은 Terraform Plan
을 실행하여 원하는 변경 사항을 보여줍니다.@slideend
어떤 일이 일어날지 보려면 먼저 terraform plan
명령을 실행하십시오 .
terraform plan
+
계획 출력에 적절한 prefix, subnet cidr이 표시되는지 확인합니다. 원한다면 terraform.tfvars
혹은 variables.tf
에 정의된 default
값을 변경해보세요.
그런 다음 terraform apply
를 실행하고 리소스가 구축되는 것을 지켜보십시오.
terraform apply
+
Terraform에서 "Do you want to perform these actions?"라는 메시지가 표시되면 yes
를 입력해야 합니다.
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
메시지를 확인하였습니까? 에러가 발생하였다면 무엇이 문제인지 찾아보세요.
지금 우리 코드는 VPC만 정의합니다. 우리는 진행되는 실습에서 이 코드와 프로비저닝된 상태 기반으로 시작 할 것입니다.
NCP Consul 화면에 접속해 보세요. 구성한 자원이 생성된 것이 확인되나요?
@slidestart blood
멱등은 수학 및 컴퓨터 과학의 특정 연산의 속성으로, 초기 적용을 넘어 동일하다면 결과를 변경하지 않고 여러 번 적용할 수 있습니다.
참고 : https://en.wikipedia.org/wiki/Idempotence
@slideend
어떤 일이 일어날지 보려면 먼저 terraform plan
명령을 실행하십시오.
terraform plan
+
VPC가 이미 구축되었으므로 Terraform은 변경이 필요하지 않다고 보고합니다.
이는 정상적이며 예상된 것입니다. 이제 다른 명령인 terraform apply
를 실행하고 지켜보십시오.
terraform apply
+
이미 올바르게 프로비저닝된 경우 VPC를 다시 생성하지 않습니다.
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
@slidestart blood
Terraform은 항상 현재 인프라를 코드에 정의된 것과 일치시키려고 합니다.
@slideend
terraform.tfvars
를 변경합니다.terraform.tfvars
파일을 편집하여 prefix
를 기존과 다른 값으로 변경합니다.
변경 후 terraform apply
를 실행하고 지켜보십시오.
terraform plan
+
VPC가 이미 구축되었으므로 Terraform은 변경이 필요하지 않다고 보고합니다.
이는 정상적이며 예상된 것입니다. 이제 다른 명령인 terraform apply
를 실행하고 지켜보십시오.
terraform apply
+
Terraform에서 "Do you want to perform these actions?"라는 메시지가 표시되면 yes
를 입력하고 완료되기를 기다립니다. 출력의 결과가 어떤가요?
2021년 10월 20일 기준으로, VPC의 이름이 변경되면 NCP에서는 이 자원을 재생성 합니다.
리소스에 대한 구성값의 변경이 유지된 채로 변경되기도 하지만, 때에 따라서는 삭제 후 재생성 합니다.
$ terraform apply
+ncloud_vpc.hashicat: Refreshing state... [id=13888]
+
+Terraform used the selected providers to generate the following execution plan.
+Resource actions are indicated with the following symbols:
+-/+ destroy and then create replacement
+
+Terraform will perform the following actions:
+
+ # ncloud_vpc.hashicat must be replaced
+-/+ resource "ncloud_vpc" "hashicat" {
+ ~ default_access_control_group_no = "26594" -> (known after apply)
+ ~ default_network_acl_no = "19325" -> (known after apply)
+ ~ default_private_route_table_no = "25834" -> (known after apply)
+ ~ default_public_route_table_no = "25833" -> (known after apply)
+ ~ id = "13888" -> (known after apply)
+ ~ name = "yourname-vpc-kr" -> "hashicat-vpc-kr" # forces replacement
+ ~ vpc_no = "13888" -> (known after apply)
+ # (1 unchanged attribute hidden)
+ }
+
+Plan: 1 to add, 0 to change, 1 to destroy.
+
+Do you want to perform these actions?
+ Terraform will perform the actions described above.
+ Only 'yes' will be accepted to approve.
+
+ Enter a value: yes
+
+ncloud_vpc.hashicat: Destroying... [id=13888]
+ncloud_vpc.hashicat: Still destroying... [id=13888, 10s elapsed]
+ncloud_vpc.hashicat: Still destroying... [id=13888, 20s elapsed]
+ncloud_vpc.hashicat: Destruction complete after 23s
+ncloud_vpc.hashicat: Creating...
+ncloud_vpc.hashicat: Still creating... [10s elapsed]
+ncloud_vpc.hashicat: Still creating... [20s elapsed]
+ncloud_vpc.hashicat: Creation complete after 23s [id=13902]
+
+Apply complete! Resources: 1 added, 0 changed, 1 destroyed.
+
@slidestart blood
@slideend
ncloud_network_acl
을 추가합니다.main.tf
파일을 열고 리소스 블록의 주석처리를 제거하려고 합니다.
리소스 유형은 ncloud_network_acl
이고 이름은 public
입니다.
각 줄의 시작 부분에서 #
문자를 제거하여 코드의 주석 처리를 제거합니다.
코드편집기에서는 주석처리를 위해 해당 라인을 선택하고 활성/비활성 할 수 있습니다.
Mac : ⌘ + /
Win : Ctrl + /
주석 제거 후 파일을 저장하세요.
변경 후 terraform apply
를 실행하고 yes
를 입력하여 추가된 리소스가 생성되는지 확인하세요.
ncloud_network_acl
리소스 내부의 vpc_no
파라메터를 확인합니다. 어떻게 가르키고 있나요?
해당 리소스는 VPC의 설정을 상속 받습니다.
Terraform은 수백개의 상호 연결되 리소스 간의 복잡한 종송석을 맵핑할 수 있습니다.
ncloud_network_acl
을 설정을 변경합니다.ncloud_network_acl
항목에 대해 description
의 내용을 수정해 보세요.
resource "ncloud_network_acl" "public" {
+ vpc_no = ncloud_vpc.hashicat.id
+ name = "${var.prefix}-acl-public"
+ description = "for Public"
+}
+
변경 후 terraform apply
를 실행하고 yes
를 입력하여 변경된 사항에 대해 리소스가 어떻게 되는지 확인하세요.
올바르게 프로비저닝된 경우 ncloud_network_acl
를 삭제 후 다시 생성하지 않습니다.
Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
@slidestart blood
-auto-approve
플래그해당 플래그를 사용하여 "Do you want to perform these actions?" 에 한 질문을 오버라이드(Override) 할 수 있습니다.
검토 단계인 Plan을 건너뛰고 바로 Create/Update/Destroy 합니다.
@slideend
main.tf
의 모든 주석을 제거하세요.outputs.tf
의 모든 주석을 제거하세요.terraform plan
를 실행하여 구성할 리소스 항목을 확인합니다.
terraform plan
+
이제 Apply를 실행하여 HashiCat 애플리케이션을 빌드합니다.
terraform apply -auto-approve
+
애플리케이션이 배포를 완료하는데 5~10분이 소요될 수 있습니다. 실행이 끝날 때 애플리케이션 URL이 포함된 Terraform 출력을 보면 완료되었음을 알 수 있습니다.
catapp_url
출력 에서 URL을 클릭하여 새 브라우저 탭에서 웹 애플리케이션을 엽니다.
경고
응용 프로그램이 로드되지 않으면 terraform apply
다시 실행 하십시오. 이렇게 하면 웹 서버를 다시 설치하고 실행 중이 아닌 경우 응용 프로그램을 시작하려고 합니다.
terraform graph를 수행합니다.
terraform graph
+
https://dreampuf.github.io/GraphvizOnline/에 앞서 digraph로 시작하는 내용을 복사하여 붙여넣고 어떤 그림이 나오는지 확인해 봅니다.
인프라에 대한 Terraform 그래프를 살펴보십시오. 종속성이 자동으로 매핑됩니다.
Terraform은 이 그래프를 사용하여 최대 효율성을 위해 병렬로 구축할 수 있는 리소스를 결정합니다.
Q. Plan 파일을 지정하지 않고 terraform apply를 실행하면 어떻게 됩니까?
이 장에서 우리는 :
Terraform을 사용하여 가상 머신 또는 컨테이너를 세우고 나면 운영 체제와 애플리케이션을 구성 할 수 있습니다.
여기에서 Provisioner 가 등장합니다.
Terraform은 Bash, Powershell, Chef, Puppet, Ansible 등을 포함한 여러 유형의 Provisioner를 지원합니다.
https://www.terraform.io/docs/provisioners/index.html
Terraform 파일 프로비저닝 도구는 원격 시스템에 파일을 복사합니다.
provisioner "file" {
+ source = "files/"
+ destination = "/home/${var.admin_username}/"
+ connection {
+ type = "ssh"
+ user = var.username
+ private_key = file(var.ssh_key)
+ host = ${self.ip}
+ }
+}
+
provisioner 블록 안에있는 코드의 connection 블록에 주목하세요. 파일 프로비저닝 도구는 SSH
, WinRM
연결을 모두 지원합니다.
Remote Exec Provisioner
를 사용하면 대상 호스트에서 스크립트 또는 기타 프로그램을 실행할 수 있습니다.
자동으로 실행할 수있는 경우 (예 : 소프트웨어 설치 프로그램) remote-exec
로 실행할 수 있습니다.
provisioner "remote-exec" {
+ inline = [
+ "sudo chown -R ${var.admin_username}:${var.admin_username} /var/www/html",
+ "chmod +x *.sh",
+ "PLACEHOLDER=${var.placeholder} WIDTH=${var.width} HEIGHT=${var.height} PREFIX=${var.prefix} ./deploy_app.sh",
+ ]
+...
+}
+
이 예에서는 일부 권한 및 소유권을 변경하고 일부 환경 변수가있는 스크립트를 실행하기 위해 몇 가지 명령을 실행합니다.
Terraform은 Chef, Puppet, Ansible과 같은 일반적인 구성 관리 도구와 잘 작동합니다.
Official Chef Terraform provisioner:
https://www.terraform.io/docs/provisioners/chef.html
Run Puppet with 'local-exec':
https://www.terraform.io/docs/provisioners/local-exec.html
Terraform and Ansible - Better Together:
https://github.com/scarolan/ansible-terraform
remote-exec
와 같은 Terraform 프로비저닝 도구는 몇 가지 간단한 명령이나 스크립트를 실행해야 할 때 유용합니다. 더 복잡한 구성 관리의 경우 Chef 또는 Ansible과 같은 도구가 필요합니다.
Provisioner는 Terraform 실행이 처음 실행될 때 만 실행됩니다. 이러한 의미에서 그 동작들은 멱등성을 띄지 않습니다.
수명이 긴 VM 또는 서버의 지속적인 상태 관리가 필요한 경우 이같은 구성 관리 도구를 활용할 수 있습니다.
반면에 변경 불가능한 인프라를 원하면 Packer 같은 이뮤터블을 위한 빌드 도구를 사용하는 것이 좋습니다.
실습을 위해 다음장으로 이동하세요.
Open Folder...
를 클릭합니다.lab02
을 열어줍니다.@slidestart blood
terraform apply
를 입력할 때마다 프로비저닝 도구가 강제로 실행되도록 몇 가지 특별한 조정을 했습니다.이는 변경할 때마다 가상 머신을 파괴하고 다시 생성하지 않고 프로비저닝 도구를 사용하여 연습할 수 있도록 하기 위한 것입니다.
triggers = {
+ build_number = timestamp()
+}
+
______________________
+< Cows love Terraform! >
+ ----------------------
+ \ ^__^
+ \ (oo)\_______
+ (__)\ )\/\
+ ||----w |
+ || ||
+=============================
+
@slideend
main.tf
파일을 열어 remote-exec
항목이 있는 곳으로 이동합니다.
inline
항목에 다음을 두줄 추가합니다.
"sudo apt -y install cowsay",
+"cowsay Mooooooooooo!",
+
팁
terraform fmt
명령을 사용하여 코드를 멋지게 정렬 할 수 있는 좋은 시간 입니다.
이제 변경 사항을 적용하십시오.
terraform apply -auto-approve
+
로그 출력을 뒤로 스크롤합니다. "Moooooooo!"라고 말하는 ASCII 아트 암소가 보일 것입니다.
@slidestart blood
출력은 실행이 끝날 때 사용자에게 유용한 정보를 전달하는 데 사용할 수 있습니다.
terraform refresh
명령은 상태 파일을 인프라에 있는 파일과 동기화합니다.이 명령은 인프라를 변경하지 않습니다.
terraform output
명령을 실행할 수 있습니다.단일 출력을 보려면 terraform output <output_name>
을 실행합니다.
@slideend
output.tf
파일을 열어 아래 항목을 추가합니다.
output "ssh_info" {
+ value = nonsensitive("sshpass -p '${data.ncloud_root_password.hashicat.root_password}' ssh root@${ncloud_public_ip.hashicat.public_ip} -oStrictHostKeyChecking=no")
+}
+
해당 output의 이름은 ssh_info
입니다.
어떤 유형의 출력이 유효한지 보려면 문서 페이지를 참조하세요.
output.tf
에 새로운 내용을 저장하고 terraform refresh
명령을 실행하여 새로운 출력을 확인합니다.
terraform refresh
+
terraform output
명령을 실행하여 모든 출력을 볼 수도 있습니다.
terraform output
+
@slidestart blood
placeholder
변수로 시도할 수 있는 다른 재미있는 사이트입니다.@slideend
Terraform 변수를 구성하는 방법에는 여러 가지가 있습니다. 지금까지 terraform.tfvars
파일을 사용하여 변수를 설정했습니다.
명령줄에서 기본값과 다른 height
, width
변수를 사용 하여 애플리케이션을 다시 배포해 보십시오.
변경 사항을 관찰하기 위해 적용할 때마다 웹 앱을 다시 로드합니다.
terraform apply -auto-approve -var height=600 -var width=800
+
다음으로 Terraform이 읽을 수 있는 환경 변수를 설정해 보십시오. 다음 명령을 실행하여 placeholder
변수를 설정합니다.
export TF_VAR_placeholder=placedog.net
+
환경 변수 적용 후 terraform apply -auto-approve
를 실행하여 다시 적용해 봅니다.
terraform apply -auto-approve
+
이제 명령줄에서 동일한 변수를 다르게 설정하여 다시 시도하십시오.
terraform apply -auto-approve -var placeholder=placebear.com
+
어떤 변수가 우선시 되었습니까? 잘 이해 되셨나요?
다음 공식문서를 참고할 수 있습니다.
Q. *.tfvars
파일과 환경 변수에 동일한 변수가 설정되어 있습니다. 어느 것이 우선합니까?
이 장에서 우리는 :
file
과 remote-exec
프로비저닝 도구에 대해 알아보았습니다.Terraform은 stateful 애플리케이션입니다. 즉, state file 내부에서 빌드 한 모든 내용을 추적합니다.
앞서의 실습에서 반복된 Apply
작업 간에 Workspace 디렉토리에 나타난 terraform.tfstate
및 terraform.tfstate.backup
파일을 보셨을 것입니다.
상태 파일은 Terraform이 알고있는 모든 것에 대한 기록 소스입니다.
파일 구조
WORKSPACE
+├── files
+│ └── deploy_app.sh
+├── main.tf
+├── outputs.tf
+├── `terraform.tfstate`
+├── `terraform.tfstate.backup`
+├── terraform.tfvars
+└── variables.tf
+
State 파일 내부는 JSON 형식으로 구성되어있습니다.
{
+ "version": 4,
+ "terraform_version": "0.12.7",
+ "serial": 14,
+ "lineage": "452b4191-89f6-db17-a3b1-4470dcb00607",
+ "outputs": {
+ "catapp_url": {
+ "value": "http://go-hashicat-5c0265179ccda553.workshop.aws.hashidemos.io",
+ "type": "string"
+ },
+
때때로 인프라는 Terraform이 통제하는 범위 밖에서 변경 될 수 있습니다. (수동으로 UI에서 변경 등)
State 파일은 인프라의 마지막으로 갱신된 상태를 나타냅니다. 상태 파일이 빌드 한 파일과 여전히 일치하는지 확인하고 확인하려면 terraform refresh
명령을 사용할 수 있습니다.
이것은 인프라를 업데이트하지 않는 상태 파일 만 업데이트합니다.
terraform refresh
+
계획을 실행하거나 적용 할 때마다 Terraform은 세 가지 데이터 소스를 조정합니다.
Terraform은 *.tf
파일에있는 내용을 기반으로 기존 리소스를 추가, 삭제, 변경 또는 교체하기 위해 최선 을 다합니다. 다음은 Plan/Apply 중에 각 리소스에 발생할 수있는 네 가지 사항입니다.
+ create
+- destroy
+-/+ replace
+~ update in-place
+
경고
무엇인가 변경할때 -/+ replace
가 발생하는지 확인하세요. 이것은 기존 리소스를 삭제하고 다시 생성합니다.
각 시나리오에서 어떤 일이 발생합니까? 논의해 볼까요?
Configuration(.tf) | State | Reality | Operation |
---|---|---|---|
ncloud_server | ??? | ||
ncloud_server | ncloud_server | ??? | |
ncloud_server | ncloud_server | ncloud_server | ??? |
ncloud_server | ncloud_server | ??? | |
ncloud_server | ??? | ||
ncloud_server | ??? |
Configuration(.tf) | State | Reality | Operation |
---|---|---|---|
ncloud_server | create | ||
ncloud_server | ncloud_server | create | |
ncloud_server | ncloud_server | ncloud_server | - |
ncloud_server | ncloud_server | delete | |
ncloud_server | - | ||
ncloud_server | update state |
Terraform Cloud는 Terraform을 사용하여 코드로 인프라를 작성하고 구축하기위한 최고의 워크 플로를 제공하는 무료 로 시작하는 SaaS 애플리케이션입니다.
Terraform Cloud는 Remote State 관리, API 기반 실행, 정책 관리 등과 같은 기능을 제공하는 호스팅 된 애플리케이션입니다. 많은 사용자가 클라우드 기반 SaaS 솔루션을 선호하는 이유 중 한가지는 인프라를 유지하여 실행하는 것이 부담될 때 입니다.
Terraform Enterprise는 동일한 애플리케이션이지만 클라우드 환경이나 데이터 센터에서 실행됩니다. 일부 사용자는 Terraform Enterprise 애플리케이션에 대한 더 많은 제어가 필요하거나 회사 방화벽 뒤의 제한된 네트워크에서 실행하려고합니다.
이 두 제품의 기능 목록은 거의 동일합니다. 다음 실습에서는 Terraform Cloud 계정을 사용할 것입니다.
기본적으로 Terraform은 랩톱 또는 워크스테이션의 Workspace 디렉토리에 State 파일을 저장합니다. 이것은 개발 및 테스트에는 괜찮지만 프로덕션 환경에서는 상태 파일을 안전하게 보호하고 저장해야합니다.
Terraform에는 상태 파일을 원격으로 저장하고 보호하는 옵션이 있습니다. Terraform Cloud 계정은 이제 오픈 소스 사용자에게도 무제한 상태 파일 스토리지를 제공합니다.
모든 상태 파일은 암호화되어 (HashiCorp Vault 사용) Terraform Cloud 계정에 안전하게 저장됩니다. 상태 파일을 다시 잃어 버리거나 삭제하는 것에 대해 걱정할 필요가 없습니다.
Local Run - Terraform 명령은 랩톱 또는 워크 스테이션에서 실행되며 모든 변수는 로컬로 구성됩니다. 테라 폼 상태 만 원격으로 저장됩니다.
Remote Run - Terraform 명령은 Terraform Cloud 컨테이너 환경에서 실행됩니다. 모든 변수는 원격 작업 공간에 저장됩니다. 코드는 Version Control System 저장소에 저장할 수 있습니다. 프리 티어 사용자의 경우 동시 실행이 1 회로 제한됩니다.
Agent Run - Terraform Cloud에서 내부네트워크에 있는 환경(VM, ldap 등)을 프로비저닝 하고자 할 때 내부에 실행을 위한 에이전트를 구성할 수 있습니다. Terraform Enterprise에서는 프로비저닝을 위한 프로세스를 여러 서버로 분산시킵니다.
실습을 위해 다음장으로 이동하세요.
Open Folder...
를 클릭합니다.lab02
을 열어줍니다.@slidestart blood
@slideend
Terraform Cloud는 다른 SaaS 서비스와 같이 개인을 위한 무료 플랜이 준비되어있습니다.
아직 계정이 없는 경우 계성을 생성하고 다음 실습을 진행합니다.
Terraform Cloud에 로그인하면 YOURNAME-training
이라는 새 조직을 만듭니다. YOURNAME
을 자신의 이름이나 다른 텍스트로 바꾸십시오.
다음으로 Workspace를 생성하라는 메시지가 표시됩니다. CLI 기반 워크플로
패널을 클릭하여 VCS 통합 단계를 건너뛸 수 있습니다.
작업 공간의 이름을 hashicat-ncp
로 지정 하고 Create workspace
를 클릭하여 새로운 Workspace를 생성합니다.
터미널에서 terraform version
을 실행하여 버전을 확인합니다.
Terraform Cloud 상에 생성한 hashicat-ncp
의 Settings > General
로 이동하여 Terraform Version
을 동일한 버전으로 구성합니다. 그리고 Execution Mode를 Local
로 설정합니다.
Settings
페이지 하단에 버튼을 클릭하여 저장합니다.@slidestart blood
@slideend
이번 실습에서는 Terraform Cloud를 Remote State Backend로 구성하여 기존 State 파일을 Terraform Cloud 환경으로 마이그레이션 합니다.
Workspace 디렉토리에 (main.tf
와 같은 위치) 아래와 같은 내용으로 remote_backend.tf
파일을 생성합니다.
# remote_backend.tf
+terraform {
+ backend "remote" {
+ hostname = "app.terraform.io"
+ organization = "YOURORGANIZATION"
+ workspaces {
+ name = "hashicat-ncp"
+ }
+ }
+}
+
YOURORGANIZATION
을 생성한 Organization 이름으로 수정합니다.
이후 터미널에서 terraform login
을 입력합니다. 로컬 환경에 Terraform Cloud와 API 인증을 위한 Token을 생성하는 과정입니다. yes
를 입력하면 Terraform Cloud의 토큰 생성화면이 열립니다.
$ terraform login
+Terraform will request an API token for app.terraform.io using your browser.
+...
+Do you want to proceed?
+ Only 'yes' will be accepted to confirm.
+
+ Enter a value:
+
Create API token
화면이 나오면 Description에 적절한 값(예: ncp workshop)을 입력한 후 버튼을 클릭하여 새로운 Token을 생성합니다.
생성된 Token을 복사하여 앞서 터미널에 새로운 입력란인 Enter a value:
에 붙여넣고 ⏎(엔터)를 입력합니다. (입력된 값은 보이지 않습니다.)
...
+Generate a token using your browser, and copy-paste it into this prompt.
+
+Terraform will store the token in plain text in the following file
+for use by subsequent commands:
+ /Users/yourname/.terraform.d/credentials.tfrc.json
+
+Token for app.terraform.io:
+ Enter a value: ******************************************
+
해당 토큰은 터미널에 표기된 credentials.tfrc.json
파일에 저장됩니다.
터미널에서 terraform init
을 실행합니다.
State를 Terraform Cloud로 마이그레이션하라는 메시지가 표시되면 "yes"를 입력합니다.
backend가 remote로 구성됨이 성공함을 확인합니다.
$ terraform init
+...
+Initializing the backend...
+
+Successfully configured the backend "remote"! Terraform will automatically
+use this backend unless the backend configuration changes.
+...
+
이제 상태가 Terraform Cloud에 안전하게 저장됩니다. TFC UI에서 작업 영역의 "State" 탭에서 이를 확인할 수 있습니다.
변수들을 변경하면서 terraform apply -auto-approve
를 실행하고, 상태 파일이 리소스가 변경될 때마다 변경되는 것을 지켜보십시오. Terraform Cloud UI를 사용하여 이전 상태 파일을 탐색할 수 있습니다.
@slidestart blood
@slideend
다음 명령을 실행하여 인프라를 삭제하세요.
terraform destroy
+
인프라를 삭제한다는 메시지가 표시되면 "yes"를 입력해야 합니다. 중요한 리소스가 실수로 삭제되는 것을 방지하기 위한 안전 기능입니다.
확인 버튼을 클릭하기 전에 리소스 삭제 작업이 완전히 끝날 때까지 기다리십시오.
HashiCorp의 제품은 설치형과 더불어 SaaS 모델로도 사용가능한 모델이 제공됩니다. 여기에는 지금까지 Terraform Cloud, HCP Vault, HCP Consul 이 제공되었습니다. HCP는 HashiCorp Cloud Platform의 약자 입니다.
여기에 최근 HCP Packer가 공식적으로 GA(General Available)되었습니다. HashiCorp의 솔루션들에 대해서 우선 OSS(Open Source Software)로 떠올려 볼 수 있지만 기업을 위해 기능이 차별화된 설치형 엔터프라이즈와 더불어 클라우드형 서비스도 제공되고 있으며 향후 새로운 솔루션들이 추가될 전망입니다.
Packer는 HashiCorp 에서 Vagrant에 이어 두번째로 릴리즈된 OSS 제품 입니다. 기존에는 표준 이미지(골든 이미지) 생성을 위해 사용자가 OS 설치 이후 접속하여 패키지, 파일, 설정 들을 수행 후 빠져나와 이미지를 생성하였다면, Packer는 미리 정의된 구성파일로 이미지를 생성하고 여러 플랫폼에 동시적으로 생성할 수 있습니다.
Amazone EC2(AMI), AZure VM, GCP GCE(Image)와 같은 주요 클라우드 밴더는 물론 국내 클라우드 밴더인 Naver Cloud Platform을 지원하고, 프라이빗 환경의 OpenStack과 VMware, 컨테이너인 Docker를 지원합니다.
플러그인이 다양하게 준비되어 있어 빌드 작업시 스크립트는 당연하고, Ansible 같은 구성관리 코드 툴과도 조합하여 이미지를 생성하는 동작을 코드화하고 자동화 합니다.
HCP Packer가 제공하는 기능은 Packer가 생성한 이미지 Metadata에 대한 Registry 기능입니다. 개념적인 이해가 필요한 부분은 Packer로 생성되는 이미지 자체는 해당 플랫폼에 저장되며 HCP Packer는 해당 이미지에 대한 정보를 저장한다는 것으로 기존 Packer OSS와 함께 사용된다는 점입니다.
이미지 Metadata에 대한 Registry로서의 기능이 서비스로 제공된다는 것이 어떤 문제를 해결하기 위함인지에 대해 이해가 필요합니다.
기업 환경에서 표준 이미지에 대한 관리 및 관련하여 Packer를 이용하면 이미지는 자동화되어 쉽게 발생하지만 작성된 이미지를 활용하는데에 어려움이 발생합니다. 몇가지 예를 들면 다음과 같은 문제점이 있습니다.
여러 문제를 해결하기 위해 Packer에서 빌드 시 HCP Packer Registry에 Metadata를 동시에 등록하고 이미지의 속성 정보를 확인할 수 있게 되어 관리성을 높이고 외부 도구에서 명확한 이미지 ID를 쉽게 얻는 인터페이스를 제공할 수 있습니다.
HCP Packer Registry의 주요 개념은 이미지 순환(Iterations)과 이미지 채널(Channels)입니다.
Packer의 빌드마다 Iterations
에 작성된 이미지의 정보가 추가됩니다.
이렇게 추가된 Iteration 정보는 빌드시마다 기록되어 기존 Packer OSS 대비 이미지 생성에 대한 기록을 확인할 수 있습니다.
각 Iteration 항목을 클릭하면 빌드의 세부적보를 확인 할 수 있고 아래 이미지에서는 AWS와 Azure에 대한 각 멀티 클라우드, 멀티 리전에 대한 생성 정보를 확인 할 수 있습니다.
Channels
는 특정 Channel에 대해 기존 작성된 Iteration을 할당할 수 있는 객체 입니다. Channel을 통해 Terraform을 포함한 외부 툴은 Iteration의 버전을 신경쓰지 않고 원하는 Channel의 이름만 알고 있으면 항상 유효한 이미지 정보를 취득할 수 있습니다. 아래 이미지에서는 Channel을 사용자가 알기 쉬운 이름으로 구성하고 작성된 Iteration 의 버전을 맵핑하는 것을 확인 할 수 있습니다.
HCP Packer에 이미지 Metadata를 등록하는 방법은 기존 Packer로 작성된 선언의 build
블록에 hip_packer_registry
속성을 정의하는 것입니다. 관련 수행을 위한 안내는 learn.hashicorp.com의 내용을 확인할 수 있습니다.
build {
+ hcp_packer_registry {
+ bucket_name = "learn-packer-ubuntu"
+ description = <<EOT
+Some nice description about the image being published to HCP Packer Registry.
+ EOT
+ bucket_labels = {
+ "owner" = "platform-team"
+ "os" = "Ubuntu",
+ "ubuntu-version" = "Focal 20.04",
+ }
+
+ build_labels = {
+ {/* "build-time" = timestamp()
+ "build-source" = basename(path.cwd) */}
+ }
+ }
+ sources = [
+ "source.amazon-ebs.basic-example-east",
+ "source.amazon-ebs.basic-example-west"
+ ]
+}
+
현재 모든 Packer Plugin이 HCP Packer를 지원하는 것은 아니므로 Plugin 페이지에서 HCP Packer Ready
표시가 되어있는지 확인이 필요합니다. 예를들어 Docker Plugin의 페이지를 확인해보면 지원되고 있는 표시를 확인 할 수 있습니다.
기업내에서는 이미지에 대한 보안 규정 준수를 위해 Image의 revoke(취소)를 지원합니다. revoke된 Iteration은 관리자에 의해 완전 삭제되지 않는다면 복구하는 것도 가능합니다. 예를 들어 작성된 이미지이용을 중단하고 싶은 경우 Revoke Immediately
요청과 관련 설명을 추가할 수 있습니다.
HCP Packer의 정보는 외부 솔루션에서도 활용 가능합니다. Terraform과의 워크플로우에서 사용시에도 hcp
프로바이더가 추가되어 저장된 정보를 데이터 소스로 활용 가능합니다.
# This assumes HCP_CLIENT_ID and HCP_CLIENT_SECRET env variables are set
+provider "hcp" { }
+
+data "hcp_packer_iteration" "ubuntu" {
+ bucket_name = "learn-packer-ubuntu"
+ channel = "development"
+}
+
+data "hcp_packer_image" "ubuntu_us_west_1" {
+ bucket_name = "learn-packer-ubuntu"
+ cloud_provider = "aws"
+ iteration_id = data.hcp_packer_iteration.ubuntu.ulid
+ region = "us-west-1"
+}
+
+output "ubuntu_iteration" {
+ value = data.hcp_packer_iteration.ubuntu
+}
+
+output "ubuntu_us_west_1" {
+ value = data.hcp_packer_image.ubuntu_us_west_1
+}
+
Terraform Cloud Business를 사용하는 경우 HCP Packer에서 제공하는 Terraform Cloud Run Tasks
기능과 통합시킬 수 있습니다. Terraform Apply시 HCP Packer에서 제공하는 Run Tasks 정책이 적용되면 Plan과 Apply 단계 중간에 명확한 이미지에 대한 확인 및 오류 메시지를 발견할 수 있습니다.
또한 이미지사 사용되는 AWS, Azure, GCP의 리소스에서 하드코딩되는 이미지 ID를 검색하거나 사용하지 못하게 경고 또는 실패하는 동작을 수행 할 수 있습니다.
무료 플랜이 제공되며 최대 10개의 이미지와 월 250건의 API 요청을 지원합니다. Standard 플랜 부터는 이미지 제한은 없고 시간 당 추적되는 이미지 총 개수와 요청 건에 대해 부과 되며 기술지원이 포함됩니다.
vault()
는 vault 연동시 사용가능 : https://www.packer.io/docs/templates/hcl_templates/functions/contextual/vault# packer build -force .
+
+locals {
+ access_key = vault("/kv-v2/data/alicloud", "access_key")
+ secret_key = vault("/kv-v2/data/alicloud", "secret_key")
+}
+
+variable "region" {
+ default = "ap-southeast-1"
+ description = "https://www.alibabacloud.com/help/doc-detail/40654.htm"
+}
+
+source "alicloud-ecs" "basic-example" {
+ access_key = local.access_key
+ secret_key = local.secret_key
+ region = var.region
+ image_name = "ssh_otp_image_1_5"
+ source_image = "centos_7_9_x64_20G_alibase_20210623.vhd"
+ ssh_username = "root"
+ instance_type = "ecs.n1.tiny"
+ io_optimized = true
+ internet_charge_type = "PayByTraffic"
+ image_force_delete = true
+}
+
+build {
+ sources = ["sources.alicloud-ecs.basic-example"]
+
+ provisioner "file" {
+ source = "./files/"
+ destination = "/tmp"
+ }
+
+# Vault OTP
+ provisioner "shell" {
+ inline = [
+ "cp /tmp/sshd /etc/pam.d/sshd",
+ "cp /tmp/sshd_config /etc/ssh/sshd_config",
+ "mkdir -p /etc/vault.d",
+ "cp /tmp/vault.hcl /etc/vault.d/vault.hcl",
+ "cp /tmp/vault-ssh-helper /usr/bin/vault-ssh-helper",
+ "/usr/bin/vault-ssh-helper -verify-only -config=/etc/vault.d/vault.hcl -dev",
+ "sudo adduser test",
+ "echo password | passwd --stdin test",
+ "echo 'test ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers",
+ "sudo sed -ie 's/SELINUX=enforcing/SELINUX=disabled /g' /etc/selinux/config"
+ ]
+ }
+
+# Apache
+ provisioner "shell" {
+ inline = [
+ "sudo yum -y update",
+ "sleep 15",
+ "sudo yum -y update",
+ "sudo yum -y install httpd",
+ "sudo systemctl enable httpd",
+ "sudo systemctl start httpd",
+ "chmod +x /tmp/deploy_app.sh",
+ "PLACEHOLDER=${var.placeholder} WIDTH=600 HEIGHT=800 PREFIX=gs /tmp/deploy_app.sh",
+ # "sudo firewall-cmd --zone=public --permanent --add-port=80/tcp",
+ # "sudo firewall-cmd --reload",
+ ]
+ }
+}
+
+variable "placeholder" {
+ default = "placekitten.com"
+ description = "Image-as-a-service URL. Some other fun ones to try are fillmurray.com, placecage.com, placebeard.it, loremflickr.com, baconmockup.com, placeimg.com, placebear.com, placeskull.com, stevensegallery.com, placedog.net"
+}
+
#!/bin/bash
+# Script to deploy a very simple web application.
+# The web app has a customizable image and some text.
+
+cat << EOM > /var/www/html/index.html
+<html>
+ <head><title>Meow!</title></head>
+ <body>
+ <div style="width:800px;margin: 0 auto">
+
+ <!-- BEGIN -->
+ <center><img src="http://${PLACEHOLDER}/${WIDTH}/${HEIGHT}"></img></center>
+ <center><h2>Meow World!</h2></center>
+ Welcome to ${PREFIX}'s app. Replace this text with your own.
+ <!-- END -->
+
+ </div>
+ </body>
+</html>
+EOM
+
+echo "Script complete."
+
vault()
는 vault 연동시 사용가능 : https://www.packer.io/docs/templates/hcl_templates/functions/contextual/vault# packer init -upgrade .
+# packer build -force .
+
+locals {
+ client_id = vault("/kv/data/azure", "client_id")
+ client_secret = vault("/kv/data/azure", "client_secret")
+ tenant_id = vault("/kv/data/azure", "tenant_id")
+ subscription_id = vault("/kv/data/azure", "subscription_id")
+ resource_group_name = var.resource_name
+ virtual_network_name = "kbid-d-krc-vnet-002"
+ virtual_network_subnet_name = "d-mgmt-snet-001"
+ virtual_network_resource_group_name = "kbid-d-krc-mgmt-rg"
+ timestamp = formatdate("YYYYMMDD_hhmmss", timeadd(timestamp(), "9h")) #생성되는 이미지 이름을 time 기반으로 생성
+}
+
+variable "placeholder" {
+ default = "placekitten.com"
+ description = "Image-as-a-service URL. Some other fun ones to try are fillmurray.com, placecage.com, placebeard.it, loremflickr.com, baconmockup.com, placeimg.com, placebear.com, placeskull.com, stevensegallery.com, placedog.net"
+}
+
+# Basic example : https://www.packer.io/docs/builders/azure/arm#basic-example
+# MS Guide : https://docs.microsoft.com/ko-kr/azure/virtual-machines/linux/build-image-with-packer
+source "azure-arm" "basic-example" {
+ client_id = local.client_id
+ client_secret = local.client_secret
+ subscription_id = local.subscription_id
+ tenant_id = local.tenant_id
+
+ # shared_image_gallery {
+ # subscription = local.subscription_id
+ # resource_group = "myrg"
+ # gallery_name = "GalleryName"
+ # image_name = "gs_pkr_${local.timestamp}"
+ # image_version = "1.0.0"
+ # }
+ managed_image_resource_group_name = local.resource_group_name
+ managed_image_name = "${var.image_name}-${local.timestamp}"
+
+ os_type = "Linux"
+ # az vm image list-publishers --location koreacentral --output table
+ image_publisher = "RedHat"
+ # az vm image list-offers --location koreacentral --publisher RedHat --output table
+ image_offer = "RHEL"
+ # az vm image list-skus --location koreacentral --publisher RedHat --offer RHEL --output table
+ image_sku = "8_4"
+
+ azure_tags = {
+ dept = "KBHC Terraform POC"
+ }
+
+ # az vm list-skus --location koreacentral --all --output table
+ build_resource_group_name = local.resource_group_name
+
+ #########################################
+ # 기존 생성되어있는 network 를 사용하기 위한 항목 #
+ #########################################
+ virtual_network_name = local.virtual_network_name
+ virtual_network_subnet_name = local.virtual_network_subnet_name
+ virtual_network_resource_group_name = local.virtual_network_resource_group_name
+
+ # location = "koreacentral"
+ vm_size = "Standard_A2_v2"
+}
+
+build {
+ sources = ["sources.azure-arm.basic-example"]
+
+ provisioner "file" {
+ source = "./files/"
+ destination = "/tmp"
+ }
+
+# Vault OTP
+ provisioner "shell" {
+ inline = [
+ "sudo cp /tmp/sshd /etc/pam.d/sshd",
+ "sudo cp /tmp/sshd_config /etc/ssh/sshd_config",
+ "sudo mkdir -p /etc/vault.d",
+ "sudo cp /tmp/vault.hcl /etc/vault.d/vault.hcl",
+ "sudo cp /tmp/vault-ssh-helper /usr/bin/vault-ssh-helper",
+ "echo \"=== Vault_Check ===\"",
+ "curl http://10.0.9.10:8200",
+ "/usr/bin/vault-ssh-helper -verify-only -config=/etc/vault.d/vault.hcl -dev",
+ "echo \"=== Add User ===\"",
+ "sudo adduser jboss",
+ "echo password | sudo passwd --stdin jboss",
+ "echo 'jboss ALL=(ALL) NOPASSWD: ALL' | sudo tee -a /etc/sudoers",
+ "echo \"=== SELINUX DISABLE ===\"",
+ "sudo sed -ie 's/SELINUX=enforcing/SELINUX=disabled /g' /etc/selinux/config"
+ ]
+ }
+
+# Apache
+ provisioner "shell" {
+ inline = [
+ "sudo yum -y update",
+ "sleep 15",
+ "sudo yum -y update",
+ "sudo yum -y install httpd",
+ "sudo systemctl enable httpd",
+ "sudo systemctl start httpd",
+ "chmod +x /tmp/deploy_app.sh",
+ "sudo PLACEHOLDER=${var.placeholder} WIDTH=600 HEIGHT=800 PREFIX=gs /tmp/deploy_app.sh",
+ "sudo firewall-cmd --zone=public --permanent --add-port=80/tcp",
+ "sudo firewall-cmd --reload",
+ ]
+ }
+}
+
#!/bin/bash
+# Script to deploy a very simple web application.
+# The web app has a customizable image and some text.
+
+cat << EOM > /var/www/html/index.html
+<html>
+ <head><title>Meow!</title></head>
+ <body>
+ <div style="width:800px;margin: 0 auto">
+
+ <!-- BEGIN -->
+ <center><img src="http://${PLACEHOLDER}/${WIDTH}/${HEIGHT}"></img></center>
+ <center><h2>Meow World!</h2></center>
+ Welcome to ${PREFIX}'s app. Replace this text with your own.
+ <!-- END -->
+
+ </div>
+ </body>
+</html>
+EOM
+
+echo "Script complete."
+
variable "base_image" {
+ default = "ubuntu-1804-bionic-v20210415"
+}
+variable "project" {
+ default = "gs-test-282101"
+}
+variable "region" {
+ default = "asia-northeast2"
+}
+variable "zone" {
+ default = "asia-northeast2-a"
+}
+variable "image_name" {
+
+}
+variable "placeholder" {
+ default = "placekitten.com"
+ description = "Image-as-a-service URL. Some other fun ones to try are fillmurray.com, placecage.com, placebeard.it, loremflickr.com, baconmockup.com, placeimg.com, placebear.com, placeskull.com, stevensegallery.com, placedog.net"
+}
+
+source "googlecompute" "basic-example" {
+ project_id = var.project
+ source_image = var.base_image
+ ssh_username = "ubuntu"
+ zone = var.zone
+ disk_size = 10
+ disk_type = "pd-ssd"
+ image_name = var.image_name
+}
+
+build {
+ name = "packer"
+ source "sources.googlecompute.basic-example" {
+ name = "packer"
+ }
+
+ provisioner "file"{
+ source = "./files"
+ destination = "/tmp/"
+ }
+
+ provisioner "shell" {
+ inline = [
+ "sudo apt-get -y update",
+ "sleep 15",
+ "sudo apt-get -y update",
+ "sudo apt-get -y install apache2",
+ "sudo systemctl enable apache2",
+ "sudo systemctl start apache2",
+ "sudo chown -R ubuntu:ubuntu /var/www/html",
+ "chmod +x /tmp/files/*.sh",
+ "PLACEHOLDER=${var.placeholder} WIDTH=600 HEIGHT=800 PREFIX=gs /tmp/files/deploy_app.sh",
+ ]
+ }
+}
+
# packer init client.pkr.hcl
+# packer build -force .
+
+variable "region" {
+ default = "ap-northeast-2"
+}
+
+variable "cni-version" {
+ default = "1.0.1"
+}
+
+packer {
+ required_plugins {
+ amazon = {
+ version = ">= 0.0.2"
+ source = "github.com/hashicorp/amazon"
+ }
+ }
+}
+
+source "amazon-ebs" "example" {
+ ami_name = "gs_demo_ubuntu_{{timestamp}}"
+ instance_type = "t3.micro"
+ region = var.region
+ source_ami_filter {
+ filters = {
+ name = "ubuntu/images/*ubuntu-bionic-18.04-amd64-server-*"
+ root-device-type = "ebs"
+ virtualization-type = "hvm"
+ }
+ most_recent = true
+ owners = ["099720109477"]
+ }
+ ssh_username = "ubuntu"
+}
+
+build {
+ sources = ["source.amazon-ebs.example"]
+
+ provisioner "file" {
+ source = "./file/"
+ destination = "/tmp"
+ }
+
+ provisioner "shell" {
+ inline = [
+ "set -x",
+ "echo Connected via Consul/Nomad client at \"${build.User}@${build.Host}:${build.Port}\"",
+ "sudo apt-get update",
+ "sudo apt-get install -y apt-transport-https ca-certificates curl gnupg lsb-release",
+ "sudo apt-get update",
+ "curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -",
+ "sudo apt-add-repository \"deb [arch=amd64] https://apt.releases.hashicorp.com bionic main\"",
+ "sudo apt-get update && sudo apt-get -y install consul nomad netcat nginx",
+ "curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -",
+ "sudo add-apt-repository \"deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable\"",
+ "sudo apt-get update",
+ "sudo apt-get install -y docker-ce openjdk-11-jdk",
+ "curl -sL -o cni-plugins.tgz https://github.com/containernetworking/plugins/releases/download/v${var.cni-version}/cni-plugins-linux-amd64-v${var.cni-version}.tgz",
+ "sudo mkdir -p /opt/cni/bin",
+ "sudo tar -C /opt/cni/bin -xzf cni-plugins.tgz",
+ ]
+ }
+}
+
variable "region" {
+ default = "ap-northeast-2"
+}
+
+variable "cni-version" {
+ default = "1.0.1"
+}
+
+locals {
+ nomad_url = "https://releases.hashicorp.com/nomad/1.2.3/nomad_1.2.3_windows_amd64.zip"
+ consul_url = "https://releases.hashicorp.com/consul/1.11.1/consul_1.11.1_windows_amd64.zip"
+ jre_url = "https://github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.13%2B8/OpenJDK11U-jre_x64_windows_hotspot_11.0.13_8.zip"
+}
+
+packer {
+ required_plugins {
+ amazon = {
+ version = ">= 0.0.2"
+ source = "github.com/hashicorp/amazon"
+ }
+ }
+}
+
+source "amazon-ebs" "example" {
+ ami_name = "gs_demo_windows_{{timestamp}}"
+ communicator = "winrm"
+ instance_type = "t2.micro"
+ region = var.region
+ source_ami_filter {
+ filters = {
+ name = "*Windows_Server-2019-English-Full-Base*"
+ root-device-type = "ebs"
+ virtualization-type = "hvm"
+ }
+ most_recent = true
+ owners = ["amazon"]
+ }
+ user_data_file = "./bootstrap_win.txt"
+ winrm_password = "SuperS3cr3t!!!!"
+ winrm_username = "Administrator"
+}
+
+build {
+ sources = ["source.amazon-ebs.example"]
+
+ provisioner "powershell" {
+ inline = [
+ "New-Item \"C:\\temp\" -ItemType Directory",
+ ]
+ }
+
+ // provisioner "file" {
+ // source = "./file/"
+ // destination = "/tmp"
+ // }
+
+ provisioner "powershell" {
+ inline = [
+ "New-Item \"C:\\hashicorp\\jre\\\" -ItemType Directory",
+ "New-Item \"C:\\hashicorp\\consul\\bin\\\" -ItemType Directory",
+ "New-Item \"C:\\hashicorp\\consul\\data\\\" -ItemType Directory",
+ "New-Item \"C:\\hashicorp\\consul\\conf\\\" -ItemType Directory",
+ "New-Item \"C:\\hashicorp\\nomad\\bin\\\" -ItemType Directory",
+ "New-Item \"C:\\hashicorp\\nomad\\data\\\" -ItemType Directory",
+ "New-Item \"C:\\hashicorp\\nomad\\conf\\\" -ItemType Directory",
+ "Invoke-WebRequest -Uri ${local.jre_url} -OutFile $env:TEMP\\jre.zip",
+ "Invoke-WebRequest -Uri ${local.consul_url} -OutFile $env:TEMP\\consul.zip",
+ "Invoke-WebRequest -Uri ${local.nomad_url} -OutFile $env:TEMP\\nomad.zip",
+ "Expand-Archive $env:TEMP\\jre.zip -DestinationPath C:\\hashicorp\\jre\\",
+ "Expand-Archive $env:TEMP\\consul.zip -DestinationPath C:\\hashicorp\\consul\\bin\\",
+ "Expand-Archive $env:TEMP\\nomad.zip -DestinationPath C:\\hashicorp\\nomad\\bin\\",
+ "[Environment]::SetEnvironmentVariable(\"Path\", $env:Path + \";C:\\hashicorp\\jre\\jdk-11.0.13+8-jre\\bin;C:\\hashicorp\\nomad\\bin;C:\\hashicorp\\consul\\bin\", \"Machine\")",
+ // "$old = (Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\\System\\CurrentControlSet\\Control\\Session Manager\\Environment' -Name path).path",
+ // "$new = \"$old;C:\\hashicorp\\jre\\jdk-11.0.13+8-jre\\bin;C:\\hashicorp\\nomad\\bin;C:\\hashicorp\\consul\\bin\"",
+ // "Set-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\\System\\CurrentControlSet\\Control\\Session Manager\\Environment' -Name path -Value $new",
+ ]
+ }
+}
+
<powershell>
+# Set administrator password
+net user Administrator SuperS3cr3t!!!!
+wmic useraccount where "name='Administrator'" set PasswordExpires=FALSE
+
+# First, make sure WinRM can't be connected to
+netsh advfirewall firewall set rule name="Windows Remote Management (HTTP-In)" new enable=yes action=block
+
+# Delete any existing WinRM listeners
+winrm delete winrm/config/listener?Address=*+Transport=HTTP 2>$Null
+winrm delete winrm/config/listener?Address=*+Transport=HTTPS 2>$Null
+
+# Disable group policies which block basic authentication and unencrypted login
+
+Set-ItemProperty -Path HKLM:\Software\Policies\Microsoft\Windows\WinRM\Client -Name AllowBasic -Value 1
+Set-ItemProperty -Path HKLM:\Software\Policies\Microsoft\Windows\WinRM\Client -Name AllowUnencryptedTraffic -Value 1
+Set-ItemProperty -Path HKLM:\Software\Policies\Microsoft\Windows\WinRM\Service -Name AllowBasic -Value 1
+Set-ItemProperty -Path HKLM:\Software\Policies\Microsoft\Windows\WinRM\Service -Name AllowUnencryptedTraffic -Value 1
+
+
+# Create a new WinRM listener and configure
+winrm create winrm/config/listener?Address=*+Transport=HTTP
+winrm set winrm/config/winrs '@{MaxMemoryPerShellMB="0"}'
+winrm set winrm/config '@{MaxTimeoutms="7200000"}'
+winrm set winrm/config/service '@{AllowUnencrypted="true"}'
+winrm set winrm/config/service '@{MaxConcurrentOperationsPerUser="12000"}'
+winrm set winrm/config/service/auth '@{Basic="true"}'
+winrm set winrm/config/client/auth '@{Basic="true"}'
+
+# Configure UAC to allow privilege elevation in remote shells
+$Key = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
+$Setting = 'LocalAccountTokenFilterPolicy'
+Set-ItemProperty -Path $Key -Name $Setting -Value 1 -Force
+
+# Configure and restart the WinRM Service; Enable the required firewall exception
+Stop-Service -Name WinRM
+Set-Service -Name WinRM -StartupType Automatic
+netsh advfirewall firewall set rule name="Windows Remote Management (HTTP-In)" new action=allow localip=any remoteip=any
+Start-Service -Name WinRM
+</powershell>
+
packer {
+ required_plugins {
+ ncloud = {
+ version = ">= 0.0.1"
+ source = "github.com/hashicorp/ncloud"
+ }
+ }
+}
+
+source "ncloud" "example-linux" {
+ access_key = var.access_key
+ secret_key = var.secret_key
+ server_image_product_code = "SPSW0LINUX000139"
+ server_product_code = "SPSVRGPUSSD00001"
+ server_image_name = var.image_name
+ server_image_description = "server image description"
+ region = "Korea"
+ communicator = "ssh"
+ ssh_username = "root"
+}
+
+build {
+ sources = ["source.ncloud.example-linux"]
+
+ provisioner "file" {
+ source = "jupyter.service"
+ destination = "/etc/systemd/system/jupyter.service"
+ }
+
+ provisioner "shell" {
+ inline = [
+ "yum clean all",
+ "yum -y install epel-release",
+ "yum -y install python3",
+ "yum -y install python-pip",
+ "pip3 install --upgrade pip",
+ "adduser jupyter",
+ "su - jupyter",
+ "pip3 install --user jupyter jupyter",
+ "systemctl enable jupyter",
+ "systemctl start jupyter"
+ ]
+ }
+}
+
+variable "access_key" {
+ type = string
+}
+
+variable "secret_key" {
+ type = string
+}
+
+variable "image_name" {
+ type = string
+ default = "test"
+}
+
# -*- mode: ruby -*-
+# vi: set ft=ruby :
+
+# base image : https://app.vagrantup.com/bento
+# Cluster IP have to set subnetting on private network subnet of VM
+
+$debianip = 50
+$centip = 60
+$suseip = 70
+
+debian_cluster = {
+ "ubuntu" => { :image => "bento/ubuntu-18.04"}
+}
+cent_cluster = {
+ "centos" => { :image => "centos/7"},
+ "rocky" => { :image => "rockylinux/8"},
+}
+suse_cluster = {
+ "suse" => { :image => "opensuse/Tumbleweed.x86_64" }
+}
+
+Vagrant.configure("2") do |config|
+
+ config.vm.synced_folder '.', '/vagrant', disabled: true
+
+ debian_cluster.each_with_index do |(hostname, info), i|
+ config.vm.define hostname do |server|
+ server.vm.box = info[:image]
+ server.vm.hostname = hostname
+ server.vm.network "private_network", name: "vboxnet1", ip: "172.28.128.#{i + $debianip}"
+
+ server.vm.provider "virtualbox" do |v|
+ v.name = hostname
+ v.gui = false
+ v.memory = 1024
+ v.cpus = 1
+
+ v.customize ["modifyvm", :id, "--vram", "9"]
+ end # end provider
+ end # end config
+ end # end cluster foreach
+
+ suse_cluster.each_with_index do |(hostname, info), i|
+ config.vm.define hostname do |server|
+ server.vm.box = info[:image]
+ server.vm.hostname = hostname
+ server.vm.network "private_network", name: "vboxnet1", ip: "172.28.128.#{i + $suseip}"
+ server.vm.provider "virtualbox" do |v|
+ v.name = hostname
+ v.gui = false
+ v.memory = 1024
+ v.cpus = 1
+
+ v.customize ["modifyvm", :id, "--vram", "9"]
+ end # end provider
+ end # end config
+ end # end cluster foreach
+
+ cent_cluster.each_with_index do |(hostname, info), i|
+ config.vm.define hostname do |server|
+ server.vm.box = info[:image]
+ server.vm.hostname = hostname
+ server.vm.network "private_network", name: "vboxnet1", ip: "172.28.128.#{i + $centip}"
+
+ server.vm.provider "virtualbox" do |v|
+ v.name = hostname
+ v.gui = false
+ v.memory = 1024
+ v.cpus = 1
+
+ v.customize ["modifyvm", :id, "--vram", "9"]
+ end # end provider
+ end # end config
+ end # end cluster foreach
+
+end
+
+
https://discuss.hashicorp.com/t/vagrant-2-2-18-osx-11-6-cannot-create-private-network/30984/9
https://discuss.hashicorp.com/t/vagran-can-not-assign-ip-address-to-virtualbox-machine/30930
테스트 환경은 MacOS이나 HashiCorp Discuss의 글을 확인해보면 Linux에서도 동일하게 발생하는 것으로 보임
기존에 VirtualBox에 Host Network Manager에서 vboxnet# 사용중
Vagrant up 시 에러 발생
VM의 Network에 Host-only Network로 해당 vboxnet#이 있어야 하나 목록에 표기 안됨
Host Network Manager에서 vboxnet#를 삭제후 다시 172.x.x.x로 생성하려고 하니 에러 발생
Stderr: VBoxManage: error: Code E_ACCESSDENIED (0x80070005) - Access denied (extended info not available)
+
기본으로 네트워크 생성시 부여받는 IP(e.g. 192.168.56.1
) 로는 가능하여 Vagrant의 구성을 해당 IP에 맞게 변경
프로비저닝과 관련하여 우리는 Day 0부터 Day 2까지의 여정이 있습니다.
우선은 프로비저닝할 준비가 되었다고 가정하고 Day 1에 드디어 인프라를 구성합니다. (VPC, Securty Group, VM, LB 등등) 그리고 이렇게 뭔가를 실행하면 실제 Day 2에서는 기존 인프라 집합에 추가로 비즈니스 요구사항에 따라 새로운 서비스를 추가하면서 그 시간이 지남에 따라 기존 인프라가 점점 변화합니다. 서비스를 제거하고 일반적으로 인프라의 모습을 발전시키는 이같은 활동은 테라폼에서 근본적으로 코드 접근 방식으로서의 인프라 즉 Infrastructure as Code 로 접근합니다.직역하면 코드가 인프라이고 인프라가 코드인 상태이죠.
일련의 선언적으로 구성파일을 정의하고 읽을수 있는 이런 구성파일로 토폴로지를 구성합니다. 테라폼 config라고 설명해놓겠습니다. 보안 그룹 규칙을 프로비저닝하거나 네트워크 보안을 설정한 다음 해당 환경 내에서 가상 머신 세트 정의를 프로비저닝하려는 VM이 있고 로드밸런서가 있고...
복잡성에 따라 매주 또는 매일 인프라가 점차 변화하고 발전합니다. 테라폼 구성으로 이런 환경을 캡쳐해두는 것은 매우 쉽고 바른 방식입니다.
그럼 테라폼 설정이 어떻게 동작하는지 알아보겠습니다.
우선 첫번째로, 테라폼은 Refresh를 통해 테라폼으로 만들어질 세상이 어떻게 생겼는지 조정합니다. 이를 통해 테라폼 View가 나오고 실제와 어떻게 다른지 비교합니다. VMware나 AWS, Azure, GCP 같은 인프라에 실제 무엇이 실행 중인지 API로 물어보고 각 상태를 확인할 수 있습니다.
플랜은 현재의 상태를 원하는 상태로 구성하는 단계 입니다. 실제 예상되는 무언가를 알려주고 우리는 미리 확인할 수 있습니다. 앞서 정의한 TF Config의 현재와 다른것이 무엇이있고 어떤 변화가 있는지 확인하고 앞으로의 변경점을 예측해주죠.
Apply는 원하는 상태를 만들기 위해 실행을 하는 단계 입니다. 필요한 것이 무엇인지 어떤것이 정의되었는지를 말이지요. 예를 들면 VM을 생성하기 전에 보안그룹을 정의하는 것 같은 순차적인 것이 무엇인지 병렬로 진행하는 것이 무엇인지를 이미 알고 있습니다. 그래프 이론에 기반한 이런 프로비저닝 방식은 사용자가 각 자원의 선후 관계를 명시하지 않아도 순차로 진행할 것과 병렬로 진행할 작업을 구분합니다.
이렇게 구성하여 우리가 원하는 인프라를 정의하고 생성하고 적용합니다.
Day 2에 들어서서 기존에 없던 로드발란서와 연결되는 DNS구성이나 CDN, 또는 내 VM을 모니터링하고 싶은 시스템에 연결하는 작업이 필요할 수 있습니다.기존에 가지고 있는 구성을 업데이트하고 다시 실행하여 처음의 리소스에 추가로 새로운 것들을 추가합니다. Day 2에 중요한 것은 계속 변화하는 환경에서 변화될 것들만 추적하고 변경할 수 있다는 것입니다. 그리고 더이상 필요하지 않을 때 원래의 상태로 돌아올 수 있습니다.
코드로 정의된 각 인프라 리소스는 Destroy를 통해 다시 제로의 상태로 돌아옵니다. 이같은 방식은 언제나 일관된 상태와 리소스 정의를 만들어줍니다.
프로바이더는 테라폼 코어와 연동되는 플러그인으로, 각 플랫폼에서 제공하거나 누구나 개발해서 테라폼과 연결할 수 있습니다. 이런 플러그인들을 프로바이더라고 부릅니다.
프로바이더는 필요에 따라 인프라, 플랫폼, 서비스를 연계해서 사용하게도 가능합니다. 예를들어 앞서 인프라를 수행했다고 보고, Day2 에 쿠버네티스 연결이 필요하면 기존 설정을 확장해서 추가 리소스와 자원을 구성합니다. 그리고 그 위에 서비스가 DNS를 요구하거나 CDN을 요구하면 이런 서비스를 추가로 애드온 하게 됩니다.
테라폼 문서에보면 이미 100여개 이상의 프로바이더가 존재하고 커뮤니티의 프로바이더 까지 합치면 테라폼으로 관리가능한 코드기반 자원들은 무궁무진합니다.
테라폼을 로컬에서 사용하는 사용자의 워크 플로우는 테라폼 구성을 실행하고나서 plan이 만들어집니다. 구성에 대한 State 관리나 변수, 각각의 설정과 관련한 코드를 로컬에서 관리됩니다.
이제 다른 팀원을 추가합니다. 우리는 인프라 작업이 일관되게 변화하는 것을 기대합니다. 새로운 VM 이 생기거나 새로운 리소스를 생기지 않도록 하려면 어떻게 해야할까요? 이런 문제는 우리가 코드 관리를 위해 코드 버전 관리 서비스를 사용하는 현상과 유사합니다. Git기반의 테라폼은 코드를 중앙에서 관리하고 이를 테라폼 엔터프라이즈에서 관리하고 워크플로우의 헛점이 발생하지 않도록 도와줍니다.
테라폼 엔터프라이즈는 VCS와 연동하여 개인 로컬 환경이 아닌 중앙에서 프로비저닝을 하도록 관리하는 역할을 합니다. 이제 로컬에서 실행하는 대신 제어시스템, github나 빗버킷이나 깃랩같은 VCS를 활용하여 중앙에서 상태관리를 합니다.
기업의 운영 환경에서 요구되는 건 또 무엇이 있을까요? 아마도 정책이 필요할 것입니다. 예를 들면 태깅을 해야한다거나 특정 리전에만 프로비저닝을 해야하는 조건을 달 수 있습니다.
또한가지, 인프라를 구성하는 작업자는 혼자서 운영할 때는 필요한 변수들을 로컬에 저장하고 활용합니다. 이런 변수에는 키같은 민감한 정보토 포함될 수 있습니다. 엔터프라이즈에서는 변수를 중앙에서 관리하고 필요한 경우 암호화 해주는 기능이 필요합니다.
이런 엔터프라이즈 기능이 워크 플로우를 관리하고 작업자가 안전하게 협업할 수 있게 도와줍니다.이렇게 반복적으로 작업이 되다 보면 일련을 동작을 모듈화하여 관리할 수 있습니다. 일종의 입출력 예제와 같이 모듈을 쉽게 정의 할 수 있습니다. 예를 들어 Java애플리케이션을 배포하고자 하는 모듈에는 배포할 Jar파일이 무엇인지, 몇개나 띄울지 물어봅니다. 이런 모듈을 기업내에서 관리하는 프라이빗 레지스트리도 기업환경에서는 요구되곤 합니다.
테라폼만으로 좋은데 엔터프라이는 왜 사용하는가에 대한 대답은 이런 협업과 정책, 관리되는 static한 변수들과 해당 조직이나 기업을 위한 모듈을 관리할 수 있도록 만들어준다는 점입니다.
이번에는 인프라의 변화와 적응이라는 제목으로 인프라의 성숙도와 관련한 이야기를 나누고자 합니다.
HashiCorp의 테라폼을 이야기하면서 함께 이야기되는 것은 언제나 Infrastructure as Code라는 IaC 입니다. 즉, 인프라는 코드로 설명되고로 테라폼은 이를 지원하는 멋진 툴입니다.
IaC는 많은 의미를 갖고 있지만, 이 이야기를 하기에 앞서 결국 이것도 코드 이기 때문에 최근의 DevOps 같은 맥락의 이야기를 할수 밖에 없을 것 같습니다.
사람이 과거에서 오늘날 까지 진화를 했든 우리의 인프라도 각 시기마다 적절한 운영 성숙도를 갖추었습니다.
첫단계에서는 매뉴얼을 사용합니다. 인프라에 대한 모든 정보와 구성 방법, 변경 방법, 기존 아키텍쳐에 대한 내용은 문서로 관리되었습니다. 여전히 엑셀 시트를 가지고 인프라 아이피와 아이디, 서비스가 어떤게 올라가 있는지 관리하는 곳도 많이 있지요. 변경에 대한 모든 사항은 문서로 남겨야 하고, 그렇지 못한다면 기억에 의존해야 합니다.
나름의 노하우를 담아 스크립팅 언어를 사용하여 작업합니다. 새로운 서버를 설정하거나 기능을 수행하기위한 작업으로 상당히 간단하고 편리합니다. 이단계에서는 비슷한 인프라나 애플리케이션 런타임이 일반적이고 조직간에 많은 의사 소통이 필요하지 않기 때문에 상당히 유용합니다. 일단 노가다를 많이 줄여줍니다. 특징은 다음과 같습니다.
이제 이미 설정된 머신을 가상화를 통해 미리 저장해두고 사용합니다. 미리 필요한 패키지나 솔루션을 설치해두고 바로 사용 가능합니다. 미리 이미지들을 만드는 작업을 수행하기 때문에 가상머신을 사용하면 자동화를 향상시킬 수 있고 공동작업도 향상됩니다.
다음은 이제 우리가 기본적으로 컨트롤 가능한 클라우드 리소스와 함께하는 시대입니다. 인프라를 더이상 소유하지 않고 상품화된 인프라, 데이터 센터를 사용하기 위해서 우리는 자동화를 요구받기 시작합니다. API를 통해 더 많은 기술 계층을 가상화 하고 더 많은 소프트웨어와 자동화 기술로 지금의 데이터 센터의 컴퓨터를 포함한 인프라를 대신하는 환경을 제공합니다.
이제 더 나아가 머신이 아닌 OS를 가상화 하기 시작합니다. 데이터 센터의 컴퓨터를 사용하는 대신 컴퓨터를 자원 풀의 하나로 여기고, 어디인지는 몰라도 애플리케이션을 적당한 리소스가 있는 그 어딘가에 배포합니다. 리소스 활용율을 높이고 이전 보다 더더더 자동화합니다. Agile이 요구되고 DevOps를 해야한다고 합니다.
더 많은 자동화를 하면 이론적으로 우리는 더 적은 일을 해야합니다. 하지만 전반적으로 자동화의 단계를 살펴보면 이론적으로 가상화를 늘리고 컨테이너화를 하는 것은 우리의 삶을 더 좋게 만들어야 하는데, 정말 그런가요? 때때로 자동화는 정말 무수히 많은 반복과 검증 작업이 필요하고 이전보다 더 많은 시간과 노력을 요구합니다. 자동화나 DevOps에대한 장난스럽지만 마음을 후비는 글들도 넘쳐나죠. 자동화를 했음에도 불구하고 다시 새로운 플랜을 실행해야 하는 상황이나 여전히 우리가 인프라를 대하는 마음가짐이 하드웨어로 바라보고 있는 시각 처럼 말입니다.
아마도 유발하라리의 소설 사피엔스를 재미있게 보신 분들은 호모사피엔스 뿐만아니라 다른 종도 함께 살았었다는 흥미로운 가정을 하는 이야기를 보셨을 것입니다. (다른종을 멸종시켰다고..) 그리고 소설에서의 이야기 처럼 앞서 다룬 자동화외에도 수십, 혹은 수백가지의 자동화를 위한 내 한몸 편하고자 하는 몸부림이 현 시점에도 병렬로 존재합니다. 퍼블릭과 프라이빗 클라우드 환경이 도입되고 있고 VM과 베어메탈 환경은 여전히 존재합니다. 더불어 더작은 엣지, IoT 인프라도 관리의 대상이 되고 있습니다.
인프라의 자동화를 이야기 함에 있어 빼뜨릴 수 없는것이 Infrastructure as Code, IoC 입니다. 코드로서의 인프라스트럭쳐, IaC가 취하는 전략이 무엇일까요? 다년간의 경험을 가진 팀이 보유한 시스템 환경을 코드로 바꾸면 무엇이 달라질까요?
그럼 몇가지 상황을 제시해보겠습니다.
코드로 인프라를 관리한다는 것은 자유롭게 변경하고 환경을 이해하고 반복적으로 동일한 상황으로 만들 수 있다는 점입니다. 그리고 그 명세를 별도의 문서로 정리할 필요 없이 명확하게 인프라가 정의되어 남아있습니다.
우선 잘 만들어지는, 좋은 인프라 자동화를 이야기 하기 앞서 그 조건을 만드는 좋은 코드의 특징을 찾아보고 몇가지 공통된 항목을 뽑아보고 다음과 같이 정리해보았습니다.
이외에도 몇가지 더 있겠지만 이런 좋은 코드의 특징과 IaC가 어떤 관계가 있을까요? 인프라도 마치 좋은 코드처럼 관리가 가능하다는 것입니다. 앞서 자동화를 위해 문서화 했어야 했고, 종속성을 분석하여 관리하고 인프라 자원의 변경이 있을때마다 변경하고 그 결과물, 혹은 결과물을 만들어내는 도구를 관리하고 다시 사용할 수 있게 만드는 것. 그것은 좋은 코드와 좋은 인프라 자동화 방식이 같은 맥락으로 이어질 수 있도록 만들어주는 IaC의 특징입니다. 하지만 '좋은' 이라는 수식이 붙는 인프라의 자동화가 그저 쉽게만 되지는 않을 수 있습니다. 물론 인프라를 위한 좋은 코드는 연습이 필요합니다.
인프라 프로비저닝의 최우선 목표는 재현 가능한 인프라를 코드로 제공하는 것입니다. DevOps팀이 CI/CD 워크플로우 내에서 익숙한 도구를 사용하여 리소스를 계획하고 프로비저닝을 할 수 있는 방법을 제공하는 것입니다. Terraform은 DevOps 팀이나 클라우드 팀에서 구성한 아키텍쳐를 코드로 템플릿화 하고 기본적인 리소스와 세분화된 프로비저닝을 처리할 수 있습니다. 이런 구성은 주요 인프라 관리 도구와 통합되고 모니터링, APM 시스템, 보안 도구, 네트워크 등을 포함하여 다른 많은 ISV 공급자의 서비스로 확장 할 수 있습니다. 정의된 템플릿은 자동화된 방식으로 필요에 따라 프로비저닝 하고, 이를 통해 Terraform은 퍼블릭과 프라이빗 클라우드에 리소스를 프로비저닝 하는 공통 워크플로우를 생성합니다.
그리고 엔터프라이즈 환경을 위한 지원은 무엇이 있을까요? Terraform Enterprise는 오픈 소스의 코드 프로비저닝으로 인프라 스트럭처에서 협업, 거버넌스 및 셀프 서비스 워크 플로우를 제공합니다. Terraform Enterprise는 팀이 협력하여 인프라를 구축 할 수 있도록 워크스페이스, 모듈과 모듈 레지스트리, 거버넌스를 위한 구성을 제공합니다.
모듈의 활용은 기존에 티켓 방식으로 수행하던 인프라 프로비저닝 워크플로우에서 인프라를 재사용 가능한 모듈에 코드로 패키지하여 개발자가 셀프 서비스 방식으로 신속하게 프로비저닝 할 수 있는 환경을 만들어 줍니다.
그리고 프로비저닝과 관련한 정책 및 코드 로깅을 통해 조직은 전체 배포를 보호, 관리 및 감사로그를 확인 할 수 있습니다.
끊임없이 성장하고 확장되는 인프라 환경에서 Infrastructure as Code라는 아이디어가 탄생했습니다. IaC에 기반한 가치는 관리되는 프로비저닝으로 기존 레거시 환경과 퍼블릭/프라이빗 클라우드에 대한 비용을 최적화하고 기존 대비 빠른 속도와 위험 감소라는 측면에서 지금의 우리가 맞이하고 있는 현 시대에 적합한 모델이라고 볼 수 있습니다. DevOps의 핵심 구성요소기기도 하나 좋은 코드가 좋은 인프라를 만드는 것처럼 품질관릴와 체계적인 거버넌스를 구성하기 위한 노력이 반드시 필요한 영역입니다.
Terraform의 가장 주요한 기능으로 Infrastructure as Code 를 이야기 할 수 있습니다. 그리고 이를 지원하는 HCL에 대해 알아보고자 합니다.
Terraform에서는 당연히 동작해야하는 필연적 기능이기 때문에 오픈소스를 포함하여 모든 유형의 Terraform에서 제공되는 기능입니다.
유형 | 지원여부 |
---|---|
Terraform OSS (Open Source Software) | ✔︎ |
Terraform Cloud | ✔︎ |
Terraform Cloud for Business | ✔︎ |
Terraform Enterprise | ✔︎ |
Infrastructure as Code에 대해 간단히 소개하자면 수작업으로 프로비저닝 하던 방식, 예를 들면 UI 클릭이나 개별적인 스크립트를 사용하여 프로비저닝하는 방식은 자동화하기 어렵고, 충분히 숙달되지 않거나 밤샘작업과 스트레스로 잠시 집중력이 떨어지면 실수가 발생할 수 있습니다. 그리고 스크립트 방식은 나름 잘 정의되어있지만 순차적으로 수행되고 중간에 오류가 나면 다시 돌이키기 힘든 방식이였습니다.
Terraform에서는 이전의 프로비저닝 방식을 개선하여 좀더 안정적인고 체계적인 관리 방식을 도입할 수 있는 메커니즘과 Infrastructure as Code의 핵심인 Code를 잘 만들고 관리할 수 있는 도구를 제공합니다.
기본적으로 Terraform은 HCL이라고하는 HashiCorp Configuration Language와 JSON가 코드의 영역을 담당하고 있습니다. 특히 HCL은 쉽게 읽을수 있고 빠르게 배울 수 있는 언어의 특성을 가지고 있습니다.
인프라가 코드로 표현되고, 이 코드는 곧 인프라이기 때문에 선언적 특성을 갖게 되고 튜링 완전한 언어적 특성을 갖습니다. 즉, 일반적인 프로그래밍 언어의 일반적인 조건문 처리같은 동작이 가능하다는 것입니다.
이렇게 코드화된 인프라는 주 목적인 자동화와 더불어 쉽게 버저닝하여 히스토리를 관리하고 함께 작업할 수 있는 기반을 제공하게 됩니다.
사실 이미 테라폼을 조금이라도 써보신분들은 당연하게도 HCL을 써보셨을 수도 있지만 처음 접하시는 경우 뭔가 또 배워야 하는건가? 어려운건가? 라는 마음의 허들이 생길 수 있습니다.
앞서 설명드렸듯 HCL은 JSON과 호환되고 이런 방식이 더 자연스러우신 분들은 JSON으로 관리가 가능합니다. 하지만 HCL에 대한 일반적인 질문은 왜 JSON이나 YAML같은 방식이 아닌지 입니다.
HCL 이전에 HashiCorp에서 사용한 도구는 Ruby같은 여타 프로그래밍 언어를 사용하여 JSON같은 구조를 만들어내기 위해 다양한 언어를 사용했습니다. 이때 알게된 점은 어떤 사용자는 인간 친화적인 언어를 원했고 어떤 사람들은 기계 친화적 언어를 원한다는 것입니다.
JSON은 이같은 요건 양쪽에 잘 맞지만 상당히 구문이 길어지고 주석이 지원되지 않는 다는 단점이 있습니다. YAML을 사용하면 처음 접하시는 분들은 실제 구조를 만들어내고 익숙해지는데 어려움이 발생하였습니다. 더군다나 일반적 프로그래밍 언어는 허용하지 않아야하는 많은 기능을 내장하고 있는 문제점도 있었습니다.
이런 여러 이유들로 JSON과 호환되는 자체 구성언어를 만들게 되었고 HCL은 사람이 쉽게 작성하고 수정할 수 있도록 설계되었습니다. 덩달아 HCL용 API가 JSON을 함께 호환하기 때문에 기계 친화적이기도 합니다.
HCL을 익히고 사용하는 건 어떨까요?
예를 들어 Python 코드로 비슷하게 정의를 내려보았습니다. 우리가 사용할 패키지를 Import하고 해당 패키지가 기본적으로 필요로하는 값을 넣어 초기화 합니다. 여기서는 aws
라는 패키지에 region
과 profile
이름을 넣어서 기본적으로 동작 할 수 있는 설정으로 초기화 하였습니다. 이후에 해당 패키지가 동작할 수 있는 여러 서브 펑션들에 대한 정의를 하고 마지막으로는 실행을 위한 큐에 넣습니다.
HCL도 거의 이런 일반적 프로그래밍의 논리와 비슷합니다. 우선 사용할 프로바이더 라고하는 마치 라이브러리나 패키지 같은 것을 정의 합니다. 이 프로바이더에는 기본적으로 선언해주어야 하는 값들이 있습니다.
그리고 이 프로바이더가 제공하는 기본적인 모듈들, 즉, 클래스나 구조체와 비슷한 형태로 정의 합니다. resource
에 대한 정의는 마치 aws_instance
라는 클래스를 example
로 정의하는 것과 비슷한 메커니즘을 갖습니다. 그리고 해당 리소스의 값들을 사용자가 재정의 하는 방식입니다.
실제 HCL의 몇가지 예는 다음과 같습니다. (github)
// 한줄 주석 방법1
+# 한줄 주석 방법2
+
+/*
+다중
+라인
+주석
+*/
+
+locals {
+ key1 = "value1" // = 를 기준으로 키와 값이 구분되며
+ key2 = "value2" // = 양쪽의 공백은 중하지 않습니다.
+ myStr = "TF ♡ UTF-8" // UTF-8 문자를 지원합니다.
+ multiStr = <<FOO // <<EOF 같은 여러줄의 스트링을 지원합니다.
+ Multi
+ Line
+ String
+ with <<ANYTEXT
+ FOO // 앞과 끝 문자만 같으면 됩니다.
+
+ boolean1 = true // boolean true
+ boolean2 = false // boolean false를 지원합니다.
+
+ deciaml = 123 // 기본적으로 숫자는 10진수,
+ octal = 0123 // 0으로 시작하는 숫자는 8진수,
+ hexadecimal = "0xD5" // 0x 값을 포함하는 스트링은 16진수,
+ scientific = 1e10 // 과학표기 법도 지원합니다.
+
+ //funtion 들이 많이 준비되어있습니다.
+ myprojectname = format("%s is myproject name", var.project)
+ //인라인 조건문도 지원합니다.
+ credentials = var.credentials == "" ? file(var.credentials_file) : var.credentials
+}
+
Terraform의 가장 기본적인 Infrastructure as Code에 대한 소개와 이를 구현하는 HCL에 대해 알아보았습니다.
Terraform의 Remote Runs이라는 기능에 대해 확인합니다.
Terraform Cloud와 Terraform Enterprise는 원격으로 트리거링 되어 동작하는 메커니즘을 제공하고 있습니다.
Enterprise에서는 수행의 주체가 중앙 서버이며, 등록된 VCS나 Terraform 설정 파일의 구성을 통해 원격으로 트리거링할 수 있습니다.
유형 | 지원여부 |
---|---|
Terraform OSS (Open Source Software) | • |
Terraform Cloud | ✔︎ |
Terraform Cloud for Business | ✔︎ |
Terraform Enterprise | ✔︎ |
원격으로 실행시키는 메커니즘은 총 3가지 형태가 있습니다.
워크스페이스는 VCS와 연동하는 것이 일반적이며, 이 경우 VCS에 Pull이 발생하면 이를 감지하여 해당 워크스페이스의 Run이 수행되는 케이스 입니다.
VCS에 Pull이 발생하는 것은 최종적으로 검증된 코드가 올라왔다고 판단되어 동작하며, 특정 파일이나 경로에 관련 동작이 발생했을 경우에만 Run이 수행되도록 설정 가능합니다.
별도의 CI/CD 파이프라인과 연계하여 사용하는 경우에는 API를 호출하여 실행시키는 방식을 제공합니다. 인프라의 변경 뿐만 아니라 애플리케이션과 상호 작용하는 경우에 유용하며, 인가된 사용자를 구분하기 위해 Token을 필요로 합니다. 주의해야 할 점은 문서상에 나와있는 것 처럼 Organization Token이 아닌 User나 Team의 Token으로 요청해야 합니다. 문서 링크
처음 워크스페이스에 Run을 요청하는 JSON 형태의 data의 예는 다음과 같습니다.
{
+ "data": {
+ "attributes": {
+ "is-destroy": false,
+ "message": "Remote Run - API"
+ },
+ "type": "runs",
+ "relationships": {
+ "workspace": {
+ "data": {
+ "type": "workspaces",
+ "id": "ws-53JSjeBcXFTCVQis"
+ }
+ }
+ }
+ }
+}
+
작성된 json파일을 POST 로 요청하는 형태는 다음과 같습니다.
curl \
+ --header "Authorization: Bearer $TF_CLOUD_TOKEN" \
+ --header "Content-Type: application/vnd.api+json" \
+ --request POST \
+ --data @run.json \
+ https://app.terraform.io/api/v2/runs
+
Run을 요청한 응답 데이터에는 관련 ìd
와 유형, 기타 정보들이 담겨있습니다.
{
+ "data": {
+ "id": "run-CZcmD7eagjhyX0vN",
+ "type": "runs",
+ "attributes": {
+ "auto-apply": false,
+ "error-text": null,
+ ...
+
Terraform 웹 콘솔에 접속하여 확인해보면 해당 ìd
값을 갖는 Run이 수행됨을 확인 할 수 있습니다.
이후 apply를 위해 comment
가 담긴 데이터를 생성하고, 앞서 응답받은 Run의 id
경로로 요청을 보냅니다.
{
+ "comment":"Looks good to me"
+}
+
curl \
+ --header "Authorization: Bearer $TF_CLOUD_TOKEN" \
+ --header "Content-Type: application/vnd.api+json" \
+ --request POST \
+ --data @apply.json \
+ https://app.terraform.io/api/v2/runs/run-CZcmD7eagjhyX0vN/actions/apply
+
API 호출을 사용하면 다양한 시스템을 활용하여 Terraform을 수행할 수 있습니다.
Terraform을 OSS로 사용하시는 분들은 커맨드 항목 중에 login
, logout
이 있는 것을 보셨을 수 있습니다. 해당 커맨드는 Terraform Cloud나 Terraform Enterprise를 활용하여 프로비저닝을 수행할 수 있는 커맨드 입니다. 원격으로 실행시키기 위해서는 우선 대상에 대한 정의가 필요합니다.
terraform {
+ backend "remote" {
+ organization = "great-stone"
+ workspaces {
+ name = "random-pet-demo"
+ }
+ }
+}
+
tf 파일에 backend
에 대한 설정을 추가하면 테라폼 실행시 원격의 서버와 통신하여 작업을 수행합니다. 워크스페이스의 경우 별도 VCS를 연동하지 않고 로컬과 연계하여 동작하게 됩니다. 또한 Apply 시 yes
에 대한 동작도 웹콘솔에서 수행던 로컬에서 수행하던 서로 동기화 되어 실행 됩니다.
VCS, API, CLI 모두 Terraform의 Run을 수행하는 동일한 동작을 수행합니다. 각 팀, 조직에서의 프로비저닝을 위한 관리 방식에 따라 가장 알맞은 방식으로 접근할 수 있는 메커니즘을 잘 활용하면 효율적인 자원 관리가 가능합니다.
Terraform을 수행하고나면 실행되고난 뒤의 상태가 저장됩니다. 로컬에서 OSS로 실행 했을 때의 terraform.tfstate
파일이 그것 입니다. 서로 다른 팀이 각자의 워크스페이스에서 작업하고 난뒤 각 상태 공유하면 변경된 내역에 따라 다음 작업을 이어갈 수 있습니다. Terraform은 Terraform Cloud, HashiCorp Consul, Amazon S3, Alibaba Cloud OSS 등에 상태 저장을 지원합니다.
Remote State, 즉 원격으로 워크스페이스의 상태 정보를 읽을 수 있다는 의미는 각 팀이 갖는 워크스페이스의 결과를 다른 팀에 노출시켜 새로 프로비저닝 된 정보를 바탕으로 다른 작업을 수행할 수 있도록 합니다. 해당 기능은 오픈소스 환경에서는 지원되지 않습니다.
유형 | 지원여부 |
---|---|
Terraform OSS (Open Source Software) | • |
Terraform Cloud | ✔︎ |
Terraform Cloud for Business | ✔︎ |
Terraform Enterprise | ✔︎ |
워크프페이스의 상태를 공유하는 워크플로우의 예를 들면 다음과 같습니다.
읽어야할 상태를 생성하는 도중에 이를 참조하는 다른 워크스페이스가 실행된다면? 이 경우 참조할 대상의 State는 잠긴 상태가 되기 때문에 해당 작업이 완료될 때까지 이를 바라보는 워크스페이스는 대기하게 됩니다.
워크스페이스가 인프라별, 혹은 프로비저닝 대상으로 인해 세분화 되는 경우에도 각 상태의 변화를 다른 워크스페이스에서 원격으로 불러옴으로서 종속적인 변경사항을 적용한 포스트 프로비저닝 프로세스가 가능하도록 하는 기능입니다.
terraform_remote_state
datasource공식 가이드에 따르면 Remote State는 terraform_remote_state
데이터소스를 통해 상태 값을 가져오게 됩니다. Remote State 가이드 보기
설정에 대한 예시와 항목에 대한 설명은 다음과 같습니다.
data "terraform_remote_state" "vpc" {
+ backend = "remote"
+
+ config = {
+ organization = "hashicorp"
+ workspaces = {
+ name = "vpc-prod"
+ }
+ }
+}
+
+# Terraform >= 0.12
+resource "aws_instance" "foo" {
+ # ...
+ subnet_id = data.terraform_remote_state.vpc.outputs.subnet_id
+}
+
+# Terraform <= 0.11
+resource "aws_instance" "foo" {
+ # ...
+ subnet_id = "${data.terraform_remote_state.vpc.subnet_id}"
+}
+
data의 항목은 terraform_remote_state
로 정의합니다. 뒤에 id 값을 임의로 넣어줍니다.
값 읽어오기는 예제에서처럼 0.12버전 이상과 0.11버전 이하로 나뉘어 호출가능하며 0.11버전 이하의 경우 output
의 데이터만 활용 가능합니다. 0.12버전 이상으로 다음의 상태값을 갖는 데이터를 예로 설명해보겠습니다.
{
+ "backend" = "remote"
+ "config" = {
+ "organization" = "great-stone"
+ "workspaces" = {
+ "name" = "terraform-examples-sensitive"
+ }
+ }
+ "outputs" = {
+ "random_server_id" = "definite-mudfish"
+ "sense" = "123456"
+ }
+ "workspace" = "default"
+}
+
상태 데이터는 Json 형태로 출력되며 마치 javascript에서 데이터를 불러오듯 활용하면 됩니다.
예를들어 config
의 워크스페이스 이름을 불러오고자 한다면
data.terraform_remote_state.<id>.config.workspaces.name
+
outputs
의 random_server_id
값을 가져오고자 하면
data.terraform_remote_state.<id>.outputs.random_server_id
+
위와 같이 데이터 구조를 확인하여 가져올 수 있습니다.
Remote State는 타 워크스페이스에서 동작한 상태 값을 기반으로 관련 데이터에 종속성이 있는 작업을 수행하기에 필요한 데이터를 제공받을 수 있는 기능입니다. 워크플로우를 정의할 때 각 팀간, 혹은 각 프로비저닝을 담당하는 주체가 서로 약속한 데이터를 주고 받을 수 있도록 코드로 정의할 수 있는 IaC의 협업 기능으로 활용 가능합니다.
Terraform은 인프라의 코드화 측면에서 그 기능을 충실히 실현해줍니다. 하지만 팀과 조직에서는 단지 인프라의 코드적 관리와 더불어 다른 기능들이 필요하기 마련입니다. 그중 하나로 정책을 꼽을 수 있습니다.
Infrastructure as Code를 구현하면 이전보다 빠른 프로비저닝이 가능합니다. 하지만, 여기에 팀웍과 조직 내 거버넌스를 유지하기 위해 또다른 도구나 무언가가 필요하다면? 관리를 위한 관리, 도구를 위한 도구들이 필요하게 될 것입니다. 그래서 테라폼의 클라우드 버전 부터는 팀이나 조직의 정책을 코드로 관리하여 자산화 시킬 수 있는 도구를 제공합니다.
유형 | 지원여부 |
---|---|
Terraform OSS (Open Source Software) | • |
Terraform Cloud | ✔︎ |
Terraform Cloud for Business | ✔︎ |
Terraform Enterprise | ✔︎ |
Sentinel 이 하시코프의 솔루션들의 정책 코드화를 위한 도구 입니다. 앞서의 IaC에 더불어 Policy 또한 코드로 정의하고 체계화 할 수 있게 도와주는 도구입니다. Sentinel 또한 VCS로 관리되고 하나 이상의 워크스페이스에 정책을 적용할 수 있게 설계 되었습니다. Sentinel의 구성은 다음과 같습니다.
└── Sentinel_Root
+ ├── sentinel.hcl
+ ├── [abc].sentinel
+ ├── [def].sentinel
+ ├── [...].sentinel
+
sentinel.hcl
에서 해당 정책 묶음을 관리합니다. 정책은 단일 또는 다중의 조건이 있으며, 각 조건은 필수인지 아닌지에 대한 조건도 있을 것입니다. 각 정책은 policy
로 .sentinel
확장자 앞의 이름이 정의 됩니다.
정책을 선언할 때 각각의 강제 정도를 정의할 수 있습니다.
policy "restrict-output-sensitive" {
+ # enforcement_level = "advisory"
+ # enforcement_level = "soft-mandatory"
+ enforcement_level = "hard-mandatory"
+}
+
정책은 Plan에 따른 state
나 config
에 작성된 조건들, 또는 시간과 같은 항목에 따라 작성가능하고, 리턴되는 값이 true
인 경우 해당 정책 조건을 만족하는 것으로 판단합니다. 작성하는 기법에 따라 여러 조건은 하나의 파일에 작성할 수도 있고 별도로 구분하여 sentinel.hcl
파일에 각각 작성하는 것도 가능합니다. 아래 예제는 테라폼 구성에 정의된 모든 output
에 대해 sensitive = true
를 강제화 하기 위한 정책 입니다.
import "tfconfig"
+
+check_outputs = func() {
+ for tfconfig.outputs as k, v {
+ if v.sensitive == false {
+ return false
+ }
+ }
+ return true
+}
+
+main = rule { check_outputs() }
+
테라폼의 IaC에 추가로 팀 내의 정책을 코드화하는, Policy as Code 의 기능으로 Sentinel을 이용하면 기존에는 별도의 문서나 구두로 존재하던 조직내 정책을 자산화 할 수 있다는 점에서 프로비저닝 관련 도구를 통합할 수 있다는 것에 의미가 있습니다.
Cloud의 경우 관련 기능을 활성화하여 1달정도 무료로 사용해 볼 수 있습니다.
Terraform은 코드로 인프라를 관리하기위한 그 '코드'의 핵심 요소인 변수처리를 다양하게 지원합니다.
Terraform에서는 다양한 변수와 작성된 변수를 관리하기 위한 메커니즘을 제공합니다. 가장 기본이되는 기능 중 하나이며 오픈소스와 엔터프라이즈 모두에서 사용가능합니다.
유형 | 지원여부 |
---|---|
Terraform OSS (Open Source Software) | ✔︎ |
Terraform Cloud | ✔︎ |
Terraform Cloud for Business | ✔︎ |
Terraform Enterprise | ✔︎ |
코드에서 변수를 사용할 수 없다면 매번 다른 값이 필요할 때마다 하드코딩된 코드 한벌씩을 만들어야 할 것입니다. 테라폼을 활용한 인프라 프로비저닝에서도 코드의 특징을 십분 활용 가능하고, 변수도 그 기능 중 하나 입니다.
테라폼 설정 파일을 작성하는 운영자와 개발자는 변수 선언을 통해 때에 따라 적절한 값을 할당하여 재 정의할 수 있습니다.
지원되는 변수의 범주와 형태는 다음과 같습니다.
기본이 되는 형태를 하나 예를 들어보겠습니다.
variable "name" { // 변수 이름
+ type = string // 변수 타입
+ description = "var String" // 변수 설명
+ default = "myString" // 변수 기본 값
+}
+
variable "이름"
으로 선언 합니다. 일반적인 코드의 int i
에서의 i
를 의미합니다. 변수 선언 시 타입과 기본 값 선언 외에도 설정 가능한 값이 많이 있어서 variable
로 선언합니다. 변수에 대한 이름 선언은 다른 위치에서 해당 변수를 사용할 수 있도록 하는 유일한 값입니다. 같은 이름의 변수를 선언하면 오류가 발생하게 됩니다.default
에 정의되는 값의 형태를 보고 자동으로 추측해주기도 하지만 변수 값의 명확한 형태를 지정해주는 것이 권장됩니다. 또한 해당 유형에 맞지 않는 값이 입력되는 것을 방지해 줍니다.type
이 없더라도 추측할 수 있습니다. default
에 설정된 값이 없고 이후 다른 코드 상에도 값이 비어 있으면 terraform이 실행 될 때 값을 물어봅니다.각 형태에 대한 예제는 아래와 같습니다.
variable "string" {
+ type = string
+ description = "var String"
+ default = "myString"
+}
+
+variable "number" {
+ type = number
+ default = "123"
+}
+
+variable "boolean" {
+ default = true
+}
+
+variable "list" {
+ default = [
+ "google",
+ "vmware",
+ "amazon",
+ "microsoft"
+ ]
+}
+
+output "list_index_0" {
+ value = var.list.0
+}
+
+output "list_all" {
+ value = [
+ for name in var.list :
+ upper(name)
+ ]
+}
+
+variable "map" { # Sorting
+ default = {
+ aws = "amazon",
+ azure = "microsoft",
+ gcp = "google"
+ }
+}
+
+output "map" {
+ value = var.map.aws
+}
+
+variable "set" { # Sorting
+ type = set(string)
+ default = [
+ "google",
+ "vmware",
+ "amazon",
+ "microsoft"
+ ]
+}
+
+output "set" {
+ value = var.set
+}
+
+variable "object" {
+ type = object({name=string, age=number})
+ default = {
+ name = "abc"
+ age = 12
+ }
+}
+
+variable "tuple" {
+ type = tuple([string, number, bool])
+ default = ["abc", 123, true]
+}
+
아직은 실험적인 Terraform의 확장 기능이지만, 변수의 유효성 체크가 가능합니다.
terraform {
+ experiments = [variable_validation]
+}
+
Terraform에서 선언되는 변수가 variable
로 되어있던 것은 관련 변수를 확장시키는데 의미가 있습니다. 변수 밖에서 별도의 로직을 답는 것이 아니라 내부에 미리 선언하여 사용 가능합니다.
variable "myVar" {
+ type = string
+ description = "for test"
+ default = "myVar"
+
+ validation {
+ condition = length(var.myVar) > 4
+ error_message = "The myVar length up to 4."
+ }
+}
+
+variable "yourVar" {
+ type = string
+ description = "for test"
+ default = "yourVar"
+
+ validation {
+ condition = can(regex("^your", var.yourVar))
+ error_message = "The yourVar must be starting with \"your\"."
+ }
+}
+
내부에 validation
를 추가하고 조건과 에러시의 메시지를 정의합니다. 조건은 true/false로 확인될 수 있는 대부분의 설정이 가능하고 string
의 경우에는 정규표현식(regex)도 확인 가능합니다.
변수를 선언하는 방식에는 여러가지가 있지만 각각의 우선순위가 있기 때문에 설정에 주의해야 합니다. 아래 순서대로 마지막에 정의된 변수가 위의 설정 값을 엎어씁니다.
-var
또는 -var-file
예를들어 myvar
변수가 정의되어있고 시스템의 환경변수로 TF_VAR_myvar
에 값이 선언되었습니다. 이후에 terraform.tfvars
에서 myvar
에 값을 선언하면 최종적으로는 terraform.tfvars
의 값이 반영됩니다.
변수에 대한 자유로운 재할당을 통해 기존 인프라에 대한 리소스, 데이터, 모듈을 변경하지 않고 이미 정해진 코드적 인프라를 재활용할 수 있습니다. 코드가 복잡해지고 여러개로 나눠진다 해도 변수에 대한 타입 정의나 조건 확인 같은 기능을 활용하고 변수가 반영 되는 우선순위를 잘 고려한다면 나이스한 IaC가 구현가능합니다.
Terraform의 워크스페이스(Workspace)는 일종의 원하는 인프라의 프로비저닝 단위로서, 하나의 state를 갖는 공간입니다. Terraform에서의 plan
혹은 apply
가 이뤄지는 단위이기도 합니다.
워크스페이스는 유형에 따라 하나의 디렉토리이거나 VCS, 혹은 VCS의 브렌치나 측정 위치가 될 수 있습니다. OSS 유형과 Cloud, Enterprise의 가장 큰 차이점은 UI로의 관리와 공동 작업을 위한 워크스페이스별 기능일 것입니다.
유형 | 지원여부 |
---|---|
Terraform OSS (Open Source Software) | ✔︎ |
Terraform Cloud | ✔︎ |
Terraform Cloud for Business | ✔︎ |
Terraform Enterprise | ✔︎ |
처음에는 워크스페이스 하나에 원하는 인프라의 모든 형태, 예를 들면 VM, 네트워크, 디스크 등의 모든 정보를 하나이 파일, 혹은 여러개의 파일로 나누어 관리합니다. 하지만 프로비저닝은 한번에 이뤄지기 때문에 하나의 작업 공간인 디렉토리 안에서 이루어 지게 됩니다.
커뮤니티 버전에서는 주로 특정 애플리케이션 인프라 구성요소를 한번에 프로비저닝 할수 있는 단위로 관리하게 되는 것이 일반적입니다. 특정 클라우드에 배포되는 특정 서비스를 위한 VM, 네트워크, 디스크 관련 내용이 모두 들있는 단일 배포 단위가 그 예입니다.
Terraform Cloud나 Enterprise에서는 각 조직 구성원이 각자의 책임에 맞도록 VM요소, 보안, 네트워크 등의 각 요소를 별도의 워크스페이스로 관리할 수 있습니다. 그 이유는 트리거링이나 워크스페이스의 결과값을 다른 워크스페이스에서 읽을 수 있는 기능들이 좀더 워크스페이스를 팀이나 조직단위로 운영할 수 있도록 제공합니다.
OSS 유형에서는 앞서의 설명처럼 Terraform이 실행되는 하나의 디렉토리 단위로 워크스페이스를 정의합니다. 기본적으로는 다음과 같은 구조를 확인 할 수 있습니다. terraform init
이후 한번 apply
한 상태의 예입니다.
└── GettingStarted
+ ├── .terraform
+ ├── main.tf
+ ├── terraform.tfstate
+ ├── terraform.tfstate.backup
+ └── variables.tf
+
Terraform은 실행시 .tf
확장자의 파일은 모두 읽어들이고 Graph Theory
에 따라 실행 순서를 결정하게 됩니다. 일반적으로 위 구조를 갖는 디렉토리가 각각의 목적에 따라 여러개를 두고 운영할 수 있고, 협업을 하는 경우 각 디렉토리를 하나의 Root를 갖는 레포지토리로 구성하거나 각각을 레포지토리로 구성하여 Git이나 SVN 같은 VCS에 저장하여 공유하게 됩니다.
Terraform Cloud와 Terraform Enterprise에서는 워크스페이스를 웹 UI를 통해 관리하게 됩니다. Terraform의 실행 주체가 로컬 환경이 아닌 리모트 환경이기 때문에 중앙에서 프로비저닝을 위한 워크스페이스를 관리하고 VCS와 연동합니다. 각 워크스페이스 마다 RBAC 적용이나 워크스페이스 트리거링 같은 조직에서 필요한 기능들이 추가됩니다.
워크스페이스는 Terraform의 기본 작업 단위로, 한번에 apply
하게되는 범위로 볼 수 있습니다. 협업을 하게 되면 워크스페이스를 git기반, 혹은 여타 VCS로 공유하여 팀 또는 조직에서의 공통 워크스페이스 관리를 하게 됩니다. 워크스페이스의 관리를 위한 기능 요건들은 Terraform Cloud, Terraform Enterprise에서 추가로 제공되는 기능으로 실제 Terraform의 실행 위치, 변수관리, 이력관리 등을 중앙에서 할 수 있다는 점에서 워크스페이스의 활용도를 높여줍니다.
Terraform Enterprise를 사용할 때, UI(https://TFE_SERVER) 상으로 접속할 수 없는 상황에서 비밀번호 변경이 필요한 경우, 아래와 같이 작업할 수 있다.
다음과 같이 수정 가능.
# 이전 버전의 TFE
+sudo docker exec -it ptfe_atlas /usr/bin/init.sh /app/scripts/wait-for-token -- bash -i -c 'cd /app && ./bin/rails c'
+## 수정 최신 버전의 TFE에서는 Container 이름이 변경됨 (2022.6.21)
+sudo docker exec -it tfe-atlas /usr/bin/init.sh /app/scripts/wait-for-token -- bash -i -c 'cd /app && ./bin/rails c'
+
irb(main):050:0> admin_user = User.find_by(username: "tfe-local-admin")
+=> #<User id: 33, email: "tfe-local-admin@test.com", username: "tfe-local-admin", is_admin: false, created_at: "2020-06-24 05:12:12", updated_at: "2020-07-01 09:12:25", suspended_at: nil, two_factor_delivery: nil, two_factor_sms_number: nil, two_factor_secret_key: nil, two_factor_recovery_index: 0, two_factor_recovery_secret_key: nil, two_factor_verified_at: nil, two_factor_enabled_at: nil, is_service_account: false, used_recovery_codes_encrypted: nil, last_auth_through_saml: nil, external_id: "user-361SGA3yMg3P1nGT", accepted_terms_at: nil, accepted_privacy_policy_at: nil, invitation_token: nil, invitation_created_at: nil, is_cyborg: false, onboarding_status: nil>
+irb(main):051:0> admin_user.password = '<<Password>>'
+=> "<<Password>>"
+irb(main):052:0> admin_user.password_confirmation = '<<Password>>'
+=> "<<Password>>"
+irb(main):053:0> admin_user.save
+2020-07-01 10:03:32 [DEBUG] {:msg=>"SettingStorage::Postgres failed to look up setting 'basic.base_domain'"}
+=> true
+
sudo docker exec -it ptfe_atlas /usr/bin/init.sh /app/scripts/wait-for-token -- bash -i -c 'cd /app && ./bin/rails c'
+user = User.find_by(email: "user@example.com")
+user.update(:password => '<<PASSWORD>>')
+user.save!
+
sudo docker exec -it ptfe_atlas /usr/bin/init.sh /app/scripts/wait-for-token -- bash -i -c 'cd /app && ./bin/rails c'
+user = User.find_by(email: "user@example.com")
+user.update(is_admin: true)
+user.save!
+
Terraform Cloud Agent(Agent)는 Terraform Enterprise/Cloud(TFE/C)에서 사용가능한 사용자 정의 Terraform 실행 환경을 제공합니다. 사용자는 Agent를 사용하여 Terraform 실행을 위해 기본 제공되는 이미지 대신 커스텀 패키지가 설치된 별도 이미지를 사용할 수 있고, 이미지 실행 위치를 네트워크 환경에서 자체 호스팅 할 수 있습니다.
Agent는 Pull 기반이므로 Agent→ TFE/C 로 네트워크 연결이 발생합니다. 실행되는 모든 Agent는 Terraform 작업 수행을 위해 TFE/C를 폴링하고 해당 작업을 로컬에서 실행합니다.
TFE/C의 제약 및 차이는 다음과 같습니다.
TFE | TFC | |
---|---|---|
지원 릴리즈 | v202109-1 | |
Agent 수 제한 | 제한 없음 | 계약에 따라 (1/3/10~) |
TFE/C 호스트 이름 등록 | TFE 호스트 이름 정의 필요 | 기본 app.terraform.io |
사용자 정의 번들 | 번들 지원 | 미지원 |
Agent 실행을 위안 네트워크 요구사항은 다음 대상으로의 Outbound가 허용되어야 합니다.
대상(Hostname) | 포트/프로토콜 | 용도 |
---|---|---|
TFE/C Hostname(e.g. app.terraform.io) | tcp/443, HTTPS | 워크로드 폴링, State 업데이트 제공, TFE/C 프라이빗 모듈 레지스트리에서 프라이빗 모듈 다운로드 |
registry.terraform.io | tcp/443, HTTPS | 공개되어있는 프로바이더 및 모듈 다운로드하기 |
releases.hashicorp.com | tcp/443, HTTPS | Terraform 바이너리 다운로드 |
archivist.terraform.io | tcp/443, HTTPS | Blob Storage |
Agent 구성을 위한 단계는 다음과 같습니다. 여기서는 사용자 정의 Agent를 포함하여 단계별로 예를 들어 설명합니다. Option
은 사용자 정의 단계로, 필요시 진행합니다.
단계 | 설명 | 구분 |
---|---|---|
1 | 사용자 정의 Agent 요건 정의 및 요청 (Option) | TF 실무자 |
2 | 사용자 정의 Agent를 생성 및 레지스트리 등록 (Option) | TFE/C 관리자 |
3 | VM 또는 K8s에 Agent 실행 환경 구성 | TFE/C 관리자 |
4 | Agent Pool 생성 | TFE/C 관리자 |
5 | Agent 실행 및 Pool 등록 확인 | TFE/C 관리자 |
6 | Workspace에서 Agent 실행 환경 설정 | TF 실무자 |
TF 실무자는 TFE/C에서 워크로드 실행시 필요한 추가 요소에 대해 정의하고, TFE/C 관리자에게 사용자 정의 Agent 생성을 요청합니다. 예를 들어 다음의 패키지/실행 요소가 필요하다고 정의 합니다.
TFE/C 관리자는 사용자 정의 Agent에 대한 요구가 있는 경우 Dockerfile
을 작성하여 TFE/C에서 사용할 이미지를 생성할 수 있습니다.
hashicorp/tfc-agent:latest
기본 이미지 사용FROM hashicorp/tfc-agent:latest
+
+# Switch the to root user in order to perform privileged actions such as
+# installing software.
+USER root
+
+# Install sudo. The container runs as a non-root user, but people may rely on
+RUN apt-get -y install sudo
+# Permit tfc-agent to use sudo apt-get commands.
+RUN echo 'tfc-agent ALL=NOPASSWD: /usr/bin/apt-get , /usr/bin/apt' >> /etc/sudoers.d/50-tfc-agent
+
+# the ability to apt-get install things.
+RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends unzip curl ca-certificates ansible jq python3-pip && wget -qO awscliv2.zip https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip && unzip awscliv2.zip && ./aws/install && rm -rf ./aws && rm -rf /var/lib/apt/lists/*
+
+# Add CA certificates and ca-trust.
+ADD cert.crt /usr/local/share/ca-certificates
+RUN update-ca-certificates
+
+# Switch back to the tfc-agent user as needed by Terraform agents.
+USER tfc-agent
+
다음과 같이 이미지 빌드를 수행합합니다.
docker build --no-cache -t cumstom.image.host/tfc-agent:v1 .
+
생성된 이미지를 관리하는 이미지 레지스트리에 생성된 이미지를 저장합니다.
docker push https://cumstom.image.host/hashicorp/tfc-agent:v1
+
Agent는 TFE/C에 내장된 기본 실행하는 기본 워커 이미지 대신 사용자 정의로 구성된 워커 이미지를 사용하도록 구성합니다. Agent는 TFE/C에 대한 지속적인 폴링을 수행하는 지속적인 실행 프로세스로 실행됩니다. 오류 발생 시 Agent가 자동으로 재시작 되도록 구성하는 것이 좋습니다.
컨테이너로 실행 되므로 Agent가 실행되기 위한 Docker, Podman 같은 컨테이너 런타임이 구성된 BM/VM 환경, Nomad, 또는 Kubernetes 환경이 필요합니다.
Terraform Agent Kubernetes Operator : https://developer.hashicorp.com/terraform/tutorials/cloud/cloud-agents
Agent를 위한 Pool을 TFE/C에 구성하고, 워크스페이스에서는 이 Pool을 선택하여 작업을 수행합니다. Agent Pool을 생성하려면 TFE/C의 Organazation의 Settings
에서 Agents
에서 수행합니다.
Create agent pool
을 선택하여 새로운 Agent Pool Name
을 입력하고 Continue
를 클릭합니다.
다음으로 Token management
에서 Description
을 입력하고 Create token
버튼을 클릭합니다.
Agent Token과 Agent를 실행하기 위한 예제 명령이 표시 됩니다. 생성된 Token은 Agent를 실행하고 구분짓는데 사용됩니다.
agent1.list
파일이라는 파일을 생성하고 아래 내용을 붙여넣습니다.
TFC_AGENT_TOKEN=<YOUR TOKEN>
+TFC_AGENT_NAME=agent1
+
이전 단계에서 만든 토큰으로 TFC_AGENT_TOKEN
의 값을 업데이트합니다. Agent의 이름은 agent1
로 지정합니다. 이 이름은 TFE/C의 Agent 관리 UI와 실행 시 표시되므로 나중에 특정 Agent를 식별할 수 있습니다.
다음과 같이 Agent를 실행합니다. 사용자 정의 Agent 이미지를 사용하는 경우 해당 이미지 주소를 넣어줍니다.
# 생성한 agent1.list 파일을 환경변수 파일로 지정하는 경우
+docker run --env-file agent1.list hashicorp/tfc-agent:latest
+
+# 환경변수를 CLI에 inline으로 지정하는 경우
+docker run -e TFC_AGENT_TOKEN -e TFC_AGENT_NAME hashicorp/tfc-agent:latest
+
사용자는 워크스페이스의 설정에서 General
항목에서 Execution Mode
를 Custom
으로 설정하고, Agent
에서 해당 워크스페이스가 실행됨을 지정할 수 있습니다. Agent
선택 시 앞서 생성된 Agent pool
의 항목을 설정합니다.
해당 워크스페이스에서 실행시 지정한 Agent에서 실행되는지 확인합니다. 아래 동작은 aws cli, python이 설치된 Agent에서 local_exec
로 버전을 확인한 결과 입니다.
본 글은 HashiCorp의 공식 워크샵인 "Intro to Terraform on Azure" 내용을 발췌하여 작성한 글입니다. 참고
실습 원본 소스코드는 hashicat-azure 저장소에서 확인할 수 있습니다.
자격증명 설정을 위한 상세 설명은 생략합니다.
Terraform에서는 해당 CSP에서 리소스를 배포하기 위해 자격증명이 필요합니다. 자신의 Azure 구독정보를 연동하기 위해 credentials를 설정합니다.
env | grep ARM
+ARM_CLIENT_ID=xxx
+ARM_CLIENT_SECRET=xxx
+ARM_SUBSCRIPTION_ID=xxx
+ARM_TENANT_ID=xxx
+
Azure에서는 기본적으로 리소스를 관리하기 위해 리소스 그룹을 생성해야 합니다. 이번 사니리오에서는 리소스 그룹을 생성해보겠습니다.
가장 기본이 되는 main.tf
코드의 구조는 다음과 같습니다.
terraform {
+ required_providers {
+ azurerm = {
+ source = "hashicorp/azurerm"
+ version = "=2.60.0"
+ }
+ }
+}
+
+provider "azurerm" {
+ features {}
+}
+
+resource "azurerm_resource_group" "myresourcegroup" {
+ name = "${var.prefix}-workshop"
+ location = var.location
+
+ tags = {}
+}
+
해당 샘플코드에서는 prefix
라는 변수만 필요하므로 다음과 같이 선언합니다.
variable "prefix" {
+ description = "This prefix will be included in the name of most resources."
+ default = "unknown"
+}
+
앞서 variables.tf
에서 default = "unknown"
으로 선언하였습니다. 이때, 사용자화된 값으로 대체하기 위해서 변수의 우선순위를 활용하여 덮어쓸 수 있습니다.
필자는 terraform.tfvars
파일을 사용하여 덮어쓰는 방식을 사용해보겠습니다.
# prefix에 자신의 이름을 작성하세요
+prefix = "hyungwook"
+
terraform init
명령을 통해 azurerm 프로바이더를 사용하기 위해 초기화를 진행합니다.terraform init
+
초기화 명령 이후에 azurerm 에서 사용할 데이터가 .terraform
디렉토리 하위에 생성되었는지 확인합니다.
ls .terraform/providers/registry.terraform.io/hashicorp
+azurerm
+
terraform plan
명령을 통해 배포되기 전 계획을 확인합니다. 해당 실습에서는 최초 배포이므로 한 개의 리소스가 create 됩니다.terraform plan
+
+Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ + create
+
+Terraform will perform the following actions:
+
+ # azurerm_resource_group.myresourcegroup will be created
+ + resource "azurerm_resource_group" "myresourcegroup" {
+ + id = (known after apply)
+ + location = "koreacentral"
+ + name = "hyungwook-workshop"
+ }
+
+Plan: 1 to add, 0 to change, 0 to destroy.
+
terraform apply
명령을 통해 실제 리소스를 배포합니다.terraform apply
+
+Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ + create
+
+Terraform will perform the following actions:
+
+ # azurerm_resource_group.myresourcegroup will be created
+ + resource "azurerm_resource_group" "myresourcegroup" {
+ + id = (known after apply)
+ + location = "koreacentral"
+ + name = "hyungwook-workshop"
+ }
+
+Plan: 1 to add, 0 to change, 0 to destroy.
+
+Do you want to perform these actions?
+ Terraform will perform the actions described above.
+ Only 'yes' will be accepted to approve.
+
+ Enter a value: yes
+
+azurerm_resource_group.myresourcegroup: Creating...
+azurerm_resource_group.myresourcegroup: Creation complete after 5s [id=/subscriptions/0222cb06-f803-4f66-a922-a0957813a90c/resourceGroups/hyungwook-workshop]
+
시나리오 1에서 생성한 리소스 그룹에 vnet을 추가합니다.
앞서 사용했던 main.tf
파일에 다음과 같이 추가할 azurerm_virtual_network
절을 추가합니다.
# 생략
+resource "azurerm_virtual_network" "vnet" {
+ name = "${var.prefix}-vnet"
+ location = azurerm_resource_group.myresourcegroup.location
+ address_space = [var.address_space]
+ resource_group_name = azurerm_resource_group.myresourcegroup.name
+}
+
prefix
: 리소스의 가장 앞에 선언할 변수명address_space
: 기본 CIDR 정의variable "prefix" {
+ description = "This prefix will be included in the name of most resources."
+ default = "unknown"
+}
+
+variable "address_space" {
+ description = "The address space that is used by the virtual network. You can supply more than one address space. Changing this forces a new resource to be created."
+ default = "10.0.0.0/16"
+}
+
azurerm_virtual_network
리소스가 추가로 생성되는 것을 확인합니다.
# 생략
+Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ + create
+
+Terraform will perform the following actions:
+
+ # azurerm_virtual_network.vnet will be created
+ + resource "azurerm_virtual_network" "vnet" {
+ + address_space = [
+ + "10.0.0.0/16",
+ ]
+ + guid = (known after apply)
+ + id = (known after apply)
+ + location = "koreacentral"
+ + name = "hyungwook-vnet"
+ + resource_group_name = "hyungwook-workshop"
+ + subnet = (known after apply)
+ + vm_protection_enabled = false
+ }
+
이번 시나리오에서는 vnet 내부에 subnet과 security group을 추가로 생성해보겠습니다.
azurerm_subnet
azurerm_network_security_group
# 생략
+resource "azurerm_subnet" "subnet" {
+ name = "${var.prefix}-subnet"
+ virtual_network_name = azurerm_virtual_network.vnet.name
+ resource_group_name = azurerm_resource_group.myresourcegroup.name
+ address_prefixes = [var.subnet_prefix]
+}
+
+resource "azurerm_network_security_group" "catapp-sg" {
+ name = "${var.prefix}-sg"
+ location = var.location
+ resource_group_name = azurerm_resource_group.myresourcegroup.name
+
+ security_rule {
+ name = "HTTP"
+ priority = 100
+ direction = "Inbound"
+ access = "Allow"
+ protocol = "Tcp"
+ source_port_range = "*"
+ destination_port_range = "80"
+ source_address_prefix = "*"
+ destination_address_prefix = "*"
+ }
+
+ security_rule {
+ name = "HTTPS"
+ priority = 102
+ direction = "Inbound"
+ access = "Allow"
+ protocol = "Tcp"
+ source_port_range = "*"
+ destination_port_range = "443"
+ source_address_prefix = "*"
+ destination_address_prefix = "*"
+ }
+
+ security_rule {
+ name = "SSH"
+ priority = 101
+ direction = "Inbound"
+ access = "Allow"
+ protocol = "Tcp"
+ source_port_range = "*"
+ destination_port_range = "22"
+ source_address_prefix = "*"
+ destination_address_prefix = "*"
+ }
+}
+
subnet_prerix
절을 추가하여 서브넷의 CIDR을 선언합니다. prefix
address_space
subnet_prefix
variable "subnet_prefix" {
+ description = "The address prefix to use for the subnet."
+ default = "10.0.10.0/24"
+}
+
다음 두 리소스가 추가적으로 생성되는 것을 확인합니다.
#(생략)
+Terraform will perform the following actions:
+
+ # azurerm_network_security_group.catapp-sg will be created
+ + resource "azurerm_network_security_group" "catapp-sg" {
+ + id = (known after apply)
+ + location = "koreacentral"
+ + name = "hyungwook-sg"
+ + resource_group_name = "hyungwook-workshop"
+ + security_rule = [
+ + {
+ + access = "Allow"
+ + description = ""
+ + destination_address_prefix = "*"
+ + destination_address_prefixes = []
+ + destination_application_security_group_ids = []
+ + destination_port_range = "22"
+ + destination_port_ranges = []
+ + direction = "Inbound"
+ + name = "SSH"
+ + priority = 101
+ + protocol = "Tcp"
+ + source_address_prefix = "*"
+ + source_address_prefixes = []
+ + source_application_security_group_ids = []
+ + source_port_range = "*"
+ + source_port_ranges = []
+ },
+#(중략)
+
+ # azurerm_subnet.subnet will be created
+ + resource "azurerm_subnet" "subnet" {
+ + address_prefix = (known after apply)
+ + address_prefixes = [
+ + "10.0.10.0/24",
+ ]
+ + enforce_private_link_endpoint_network_policies = false
+ + enforce_private_link_service_network_policies = false
+ + id = (known after apply)
+ + name = "hyungwook-subnet"
+ + resource_group_name = "hyungwook-workshop"
+ + virtual_network_name = "hyungwook-vnet"
+ }
+
+Plan: 2 to add, 0 to change, 0 to destroy.
+
참고 : 원본 소스코드는 hashicat-azure 저장소에서 확인할 수 있습니다.
이번 시나리오는 실제 VM 인스턴스에 초기화 스크립트를 사용하여 웹 애플리케이션을 배포해보도록 하겠습니다.
azurerm_network_interface
: Network Interface 생성azurerm_network_interface_security_group_association
: Network Interface에 Security Group 할당azurerm_public_ip
: 가상머신에 접속하기 위한 Public IPazurerm_virtual_machine
: 가상머신null_resource
: 가상머신 배포 후 provisioner
를 통해 웹 서비스 설치를 위해 사용terraform {
+ required_providers {
+ azurerm = {
+ source = "hashicorp/azurerm"
+ version = "=2.60.0"
+ }
+ }
+}
+
+provider "azurerm" {
+ features {}
+}
+
+resource "azurerm_resource_group" "myresourcegroup" {
+ name = "${var.prefix}-workshop"
+ location = var.location
+
+ tags = {
+ environment = "Production"
+ }
+}
+
+resource "azurerm_virtual_network" "vnet" {
+ name = "${var.prefix}-vnet"
+ location = azurerm_resource_group.myresourcegroup.location
+ address_space = [var.address_space]
+ resource_group_name = azurerm_resource_group.myresourcegroup.name
+}
+
+resource "azurerm_subnet" "subnet" {
+ name = "${var.prefix}-subnet"
+ virtual_network_name = azurerm_virtual_network.vnet.name
+ resource_group_name = azurerm_resource_group.myresourcegroup.name
+ address_prefixes = [var.subnet_prefix]
+}
+
+resource "azurerm_network_security_group" "catapp-sg" {
+ name = "${var.prefix}-sg"
+ location = var.location
+ resource_group_name = azurerm_resource_group.myresourcegroup.name
+
+ security_rule {
+ name = "HTTP"
+ priority = 100
+ direction = "Inbound"
+ access = "Allow"
+ protocol = "Tcp"
+ source_port_range = "*"
+ destination_port_range = "80"
+ source_address_prefix = "*"
+ destination_address_prefix = "*"
+ }
+
+ security_rule {
+ name = "HTTPS"
+ priority = 102
+ direction = "Inbound"
+ access = "Allow"
+ protocol = "Tcp"
+ source_port_range = "*"
+ destination_port_range = "443"
+ source_address_prefix = "*"
+ destination_address_prefix = "*"
+ }
+
+ security_rule {
+ name = "SSH"
+ priority = 101
+ direction = "Inbound"
+ access = "Allow"
+ protocol = "Tcp"
+ source_port_range = "*"
+ destination_port_range = "22"
+ source_address_prefix = "*"
+ destination_address_prefix = "*"
+ }
+}
+
+resource "azurerm_network_interface" "catapp-nic" {
+ name = "${var.prefix}-catapp-nic"
+ location = var.location
+ resource_group_name = azurerm_resource_group.myresourcegroup.name
+
+ ip_configuration {
+ name = "${var.prefix}ipconfig"
+ subnet_id = azurerm_subnet.subnet.id
+ private_ip_address_allocation = "Dynamic"
+ public_ip_address_id = azurerm_public_ip.catapp-pip.id
+ }
+}
+
+resource "azurerm_network_interface_security_group_association" "catapp-nic-sg-ass" {
+ network_interface_id = azurerm_network_interface.catapp-nic.id
+ network_security_group_id = azurerm_network_security_group.catapp-sg.id
+}
+
+resource "azurerm_public_ip" "catapp-pip" {
+ name = "${var.prefix}-ip"
+ location = var.location
+ resource_group_name = azurerm_resource_group.myresourcegroup.name
+ allocation_method = "Dynamic"
+ domain_name_label = "${var.prefix}-meow"
+}
+
+resource "azurerm_virtual_machine" "catapp" {
+ name = "${var.prefix}-meow"
+ location = var.location
+ resource_group_name = azurerm_resource_group.myresourcegroup.name
+ vm_size = var.vm_size
+
+ network_interface_ids = [azurerm_network_interface.catapp-nic.id]
+ delete_os_disk_on_termination = "true"
+
+ storage_image_reference {
+ publisher = var.image_publisher
+ offer = var.image_offer
+ sku = var.image_sku
+ version = var.image_version
+ }
+
+ storage_os_disk {
+ name = "${var.prefix}-osdisk"
+ managed_disk_type = "Standard_LRS"
+ caching = "ReadWrite"
+ create_option = "FromImage"
+ }
+
+ os_profile {
+ computer_name = var.prefix
+ admin_username = var.admin_username
+ admin_password = var.admin_password
+ }
+
+ os_profile_linux_config {
+ disable_password_authentication = false
+ }
+
+ tags = {}
+
+ # Added to allow destroy to work correctly.
+ depends_on = [azurerm_network_interface_security_group_association.catapp-nic-sg-ass]
+}
+
+resource "null_resource" "configure-cat-app" {
+ depends_on = [
+ azurerm_virtual_machine.catapp,
+ ]
+
+ # Terraform 0.12
+ triggers = {
+ build_number = timestamp()
+ }
+
+ provisioner "file" {
+ source = "files/"
+ destination = "/home/${var.admin_username}/"
+
+ connection {
+ type = "ssh"
+ user = var.admin_username
+ password = var.admin_password
+ host = azurerm_public_ip.catapp-pip.fqdn
+ }
+ }
+
+ provisioner "remote-exec" {
+ inline = [
+ "sudo apt -y update",
+ "sleep 15",
+ "sudo apt -y update",
+ "sudo apt -y install apache2",
+ "sudo systemctl start apache2",
+ "sudo chown -R ${var.admin_username}:${var.admin_username} /var/www/html",
+ "chmod +x *.sh",
+ "PLACEHOLDER=${var.placeholder} WIDTH=${var.width} HEIGHT=${var.height} PREFIX=${var.prefix} ./deploy_app.sh",
+ ]
+
+ connection {
+ type = "ssh"
+ user = var.admin_username
+ password = var.admin_password
+ host = azurerm_public_ip.catapp-pip.fqdn
+ }
+ }
+}
+
+
variable "prefix" {
+ description = "This prefix will be included in the name of most resources."
+ default = "unknown"
+}
+
+variable "location" {
+ description = "The region where the virtual network is created."
+ default = "eastus"
+}
+
+variable "address_space" {
+ description = "The address space that is used by the virtual network. You can supply more than one address space. Changing this forces a new resource to be created."
+ default = "10.0.0.0/16"
+}
+
+variable "subnet_prefix" {
+ description = "The address prefix to use for the subnet."
+ default = "10.0.10.0/24"
+}
+
+variable "vm_size" {
+ description = "Specifies the size of the virtual machine."
+ default = "Standard_B1s"
+}
+
+variable "image_publisher" {
+ description = "Name of the publisher of the image (az vm image list)"
+ default = "Canonical"
+}
+
+variable "image_offer" {
+ description = "Name of the offer (az vm image list)"
+ default = "0001-com-ubuntu-server-jammy"
+}
+
+variable "image_sku" {
+ description = "Image SKU to apply (az vm image list)"
+ default = "22_04-LTS-gen2"
+}
+
+variable "image_version" {
+ description = "Version of the image to apply (az vm image list)"
+ default = "latest"
+}
+
+variable "admin_username" {
+ description = "Administrator user name for linux and mysql"
+ default = "hashicorp"
+}
+
+variable "admin_password" {
+ description = "Administrator password for linux and mysql"
+ default = "Password123!"
+}
+
+variable "height" {
+ default = "400"
+ description = "Image height in pixels."
+}
+
+variable "width" {
+ default = "600"
+ description = "Image width in pixels."
+}
+
+variable "placeholder" {
+ default = "placekitten.com"
+ description = "Image-as-a-service URL. Some other fun ones to try are fillmurray.com, placecage.com, placebeard.it, loremflickr.com, baconmockup.com, placeimg.com, placebear.com, placeskull.com, stevensegallery.com, placedog.net"
+}
+
main.tf
에서 추가했던 각종 리소스가 추가적으로 배포되는 것을 확인합니다.
해당 시나리오에서는 가상머신 생성 후 null_resource
리소스를 통해 아파치 웹 서버를 설치하는 과정이 포함되어 있으므로 3~5분정도 소요됩니다.
# 생략
+null_resource.configure-cat-app (remote-exec): Script complete.
+null_resource.configure-cat-app: Creation complete after 31s [id=7198378208770846330]
+
+Apply complete! Resources: 1 added, 0 changed, 1 destroyed.
+
+Outputs:
+
+catapp_url = "http://hyungwook-meow.koreacentral.cloudapp.azure.com"
+
catapp_url
을 통해 접속해본 결과 정상적으로 웹 애플리케이션이 배포되고 고양이 이미지를 출력하는 것을 확인할 수 있다.
본 실습을 통해서 간략하게 Azure의 azurerm 프로바이더를 통해 각종 리소스를 생성하는 방안을 알아보았습니다.
잘못된 정보나 수정이 필요한 부분이 있다면 언제든 피드백 부탁드립니다!
작성자 : 유형욱(hyungwook.yu@hashicorp.com)
+resource "aws_vpc" "nomad_demo" {
+ cidr_block = var.vpc_cidr_block
+ #dns 권한설정이 필요함
+ enable_dns_support = true
+ enable_dns_hostnames = true
+ tags = {
+ env = "nomad"
+ }
+}
+
+resource "aws_subnet" "nomad_demo" {
+ cidr_block = var.vpc_cidr_block
+ vpc_id = aws_vpc.nomad_demo.id
+ #efs와 az가 같아야함
+ availability_zone = "ap-northeast-2a"
+}
+
+############
+# Policy
+
+data "aws_iam_policy_document" "instance_role" {
+ statement {
+ effect = "Allow"
+ actions = [
+ "sts:AssumeRole",
+ ]
+
+ principals {
+ type = "Service"
+ identifiers = ["ec2.amazonaws.com"]
+ }
+ }
+}
+
+resource "aws_iam_role" "instance_role" {
+ name_prefix = "${var.prefix}-nomad"
+ assume_role_policy = data.aws_iam_policy_document.instance_role.json
+}
+
+resource "aws_iam_role" "instance_role" {
+ name_prefix = "${var.prefix}-nomad"
+ assume_role_policy = data.aws_iam_policy_document.instance_role.json
+}
+
+resource "aws_iam_instance_profile" "test_profile" {
+ name = "test_profile"
+ role = aws_iam_role.instance_role.name
+}
+
+resource "aws_iam_role_policy" "cluster_discovery" {
+ name = "${var.prefix}-nomad-cluster_discovery"
+ role = aws_iam_role.instance_role.id
+ policy = data.aws_iam_policy_document.cluster_discovery.json
+}
+
+data "aws_iam_policy_document" "cluster_discovery" {
+ # allow role with this policy to do the following: list instances, list tags, autoscale
+ statement {
+ effect = "Allow"
+ actions = [
+ "ec2:DescribeInstances",
+ "autoscaling:CompleteLifecycleAction",
+ "ec2:DescribeTags",
+ "ecs:ListClusters",
+ "ecs:DescribeClusters",
+ "ecs:DeregisterContainerInstance",
+ "ecs:ListContainerInstances",
+ "ecs:RegisterContainerInstance",
+ "ecs:SubmitContainerStateChange",
+ "ecs:SubmitTaskStateChange",
+ "ecs:DescribeContainerInstances",
+ "ecs:DescribeTasks",
+ "ecs:ListTasks",
+ "ecs:UpdateContainerAgent",
+ "ecs:StartTask",
+ "ecs:StopTask",
+ "ecs:RunTask",
+ "elasticfilesystem:ClientMount",
+ "elasticfilesystem:ClientWrite",
+ "elasticfilesystem:ClientRootAccess"
+ ]
+ resources = ["*"]
+ }
+}
+
+############
+#EFS
+
+resource "aws_iam_role_policy" "mount_efs_volumes" {
+ name = "mount-efs-volumes"
+ role = aws_iam_role.instance_role.id
+ policy = data.aws_iam_policy_document.mount_efs_volumes.json
+}
+
+data "aws_iam_policy_document" "mount_efs_volumes" {
+ statement {
+ effect = "Allow"
+
+ actions = [
+ "ec2:DescribeInstances",
+ "ec2:DescribeTags",
+ "ec2:DescribeVolumes",
+ "ec2:AttachVolume",
+ "ec2:DetachVolume",
+ ]
+ resources = ["*"]
+ }
+}
+
+# csi efs volume
+resource "aws_efs_file_system" "nomad_csi" {
+ creation_token = "nomad-csi"
+ performance_mode = "generalPurpose"
+ throughput_mode = "bursting"
+
+ tags = {
+ Name = "nomad-csi"
+ }
+ #az가 ec2와 동일해야함
+ availability_zone_name = "ap-northeast-2a"
+}
+
+#ec2와 subnet이 같아야함
+resource "aws_efs_mount_target" "nomad_efs" {
+ file_system_id = aws_efs_file_system.nomad_csi.id
+ subnet_id = aws_subnet.nomad_demo.id
+ security_groups = [ aws_security_group.efs.id ]
+}
+
+resource "aws_security_group" "efs" {
+ name = "allow_efs"
+ description = "Allow EFS inbound traffic"
+ vpc_id = aws_vpc.nomad_demo.id
+
+ ingress {
+ description = "TLS from VPC"
+ from_port = 443
+ to_port = 443
+ protocol = "tcp"
+ cidr_blocks = [ "0.0.0.0/0" ]
+ }
+
+ egress {
+ from_port = 0
+ to_port = 0
+ protocol = "-1"
+ cidr_blocks = ["0.0.0.0/0"]
+ }
+
+ tags = {
+ Name = "allow_tls"
+ }
+}
+
#efs csi job을 생성
+resource "nomad_job" "nomad_csi_node_job" {
+ jobspec = file("./job_file/csi-node.tpl")
+}
+
+resource "time_sleep" "wait_30_seconds" {
+ depends_on = [nomad_job.nomad_csi_node_job]
+ create_duration = "30s"
+}
+
+#생성된 plugin을 기다림
+data "nomad_plugin" "efs" {
+ depends_on = [time_sleep.wait_30_seconds]
+ plugin_id = "aws-efs0"
+ wait_for_registration = true
+}
+
+#efs volume을 nomad에서 사용할 수 있게 plugins을 생성
+resource "nomad_volume" "efs_csi_volume" {
+ depends_on = [data.nomad_plugin.efs]
+ type = "csi"
+ plugin_id = "aws-efs0"
+ volume_id = "efs_csi_volume"
+ name = "efs_csi_volume"
+ external_id = data.terraform_remote_state.net.outputs.nomad_efs_name.id
+
+ capability {
+ access_mode = "single-node-writer"
+ attachment_mode = "file-system"
+ }
+
+ mount_options {
+ fs_type = "ext4"
+ }
+}
+
job "efs_csi_job" {
+ datacenters = ["dc1"]
+
+ type = "system"
+
+ group "cache" {
+ count = 1
+
+ network {
+ port "db" {
+ to = 6379
+ }
+ }
+ # 생성한 volume id 값을 명시한 volume을 선언
+ volume "cache" {
+ type = "csi"
+ source = "efs_csi_volume"
+ attachment_mode = "file-system"
+ access_mode = "single-node-writer"
+ read_only = false
+ }
+
+ task "redis" {
+ driver = "docker"
+
+ config {
+ image = "redis:6.2.6-alpine3.15"
+ ports = ["db"]
+ }
+
+ resources {
+ cpu = 500
+ memory = 511
+ }
+ #선언한 volume을 사용할 위치에 mount
+ volume_mount {
+ volume = "cache"
+ destination = "/data"
+ read_only = false
+ }
+ }
+ }
+}
+
Log |
---|
Error : compute.VirtualMachinesClient#CreateOrUpdate: Failure sending request: StatusCode=400 – Original Error: Code=“InvalidParameter” Message=“The Admin Username specified is not allowed.” Target="adminUsername" |
Azure(azurerm) 프로바이더를 사용하여 Virtual Machine을 프로비저닝하는 경우 OSProfile
에서 Admin User Name을 잘못된 조건으로 구성하는 경우 발생 할 수 있음
Azure의 API에서 정의하는 OSProfile
내의 AdminUsername
은 온라인 문서에서처럼 몇가지 룰이 있다.
.
으로 끝날 수 없음https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/virtual_machine
resource "azurerm_virtual_machine" "main" {
+ name = "${var.prefix}-vm"
+ location = azurerm_resource_group.main.location
+ resource_group_name = azurerm_resource_group.main.name
+ network_interface_ids = [azurerm_network_interface.main.id]
+ vm_size = "Standard_DS1_v2"
+
+ storage_image_reference {
+ publisher = "Canonical"
+ offer = "UbuntuServer"
+ sku = "16.04-LTS"
+ version = "latest"
+ }
+ storage_os_disk {
+ name = "myosdisk1"
+ caching = "ReadWrite"
+ create_option = "FromImage"
+ managed_disk_type = "Standard_LRS"
+ }
+ os_profile {
+ computer_name = "hostname"
+ admin_username = "test"
+ admin_password = "Password1234!"
+ }
+ os_profile_linux_config {
+ disable_password_authentication = false
+ }
+ tags = {
+ environment = "staging"
+ }
+}
+
현상
... googleapi: Error 400: Invalid request: Invalid request since instance is not running.
+
: Terraform을 통하지 않고 리소스가 삭제되어, 해당 리소스를 찾지 못하는 상황 발생
State 삭제
Local 환경의 terraform에 remote를 Terraform cloud로 지정
terraform {
+ required_version = ">= 0.12"
+ backend "remote" {
+ hostname = "app.terraform.io"
+ organization = "lguplus"
+
+ workspaces {
+ name = "kids_library"
+ }
+ }
+}
+
state 리스트 확인 terraform state list
my-workspace > terraform state list
+random_pet.sql
+module.Cluster_GKE.google_container_cluster.k8sexample
+module.Cluster_GKE.google_container_node_pool.pool_1
+module.Cluster_GKE.google_container_node_pool.pool_2
+module.gcs_buckets.google_storage_bucket.buckets[0]
+module.sql-db.google_sql_database.default
+module.sql-db.google_sql_database_instance.default
+module.sql-db.google_sql_user.default
+module.sql-db.null_resource.module_depends_on
+module.sql-db.random_id.user-password
+module.network.module.routes.google_compute_route.route["egress-internet"]
+module.network.module.subnets.google_compute_subnetwork.subnetwork["asia-northeast3/fc-kidslib-stg-subnet-1"]
+module.network.module.vpc.google_compute_network.network
+
존재하지 않는 resource를 삭제 terraform state rm [resource_name]
my-workspace > terraform state rm module.sql-db
+Removed module.sql-db.google_sql_database.default
+Removed module.sql-db.google_sql_database_instance.default
+Removed module.sql-db.google_sql_user.default
+Removed module.sql-db.null_resource.module_depends_on
+Removed module.sql-db.random_id.user-password
+Successfully removed 5 resource instance(s).
+
v202111-1 (582) 버전 이상으로 설치, 또는 업그레이드 시 발생하는 이슈
Nginx access Log |
---|
2021/12/17 02:58:31 [error] 10#10: *913 connect(0mfailed (111: Connection refused) while connecting to upstream, client: 10.10.10.100, server:tfe.mydomain.com, reguest: "GET / HTTP/1.1", upstream: "http://172.11.0.1:9292/", host: "tfe.mydomain.com" |
Ptfe_atlas Log |
---|
ExecJS::RuntimeUnavailable: Could not find a JavaScript runtime. see https://github.com/rails/execjs for a list fo available runtimes. |
v202111-1 (582) Release를 기점으로 TFE 내 포함된 container 들에 대한 update 가 있었으며, 해당 update 는 Alpine 3.14 를 사용하게 되는데 이는 특정 버전 이상의 'docker' 와 'libsecomp' 를 요구합니다. (docker - 20.10.0 이상 / libsecomp - 2.4.4 이상)
Log |
---|
Error: state snapshot was created by Terraform v0.13.2, which is newer than current v0.12.26; upgrade to Terraform v0.13.2 or greater to work with this state |
terraform_remote_state
는 버전에 관계 없이 워크스페이스 간에 output을 읽어올 수 있음을 확인따라서, 관련 에러가 발생한 워크스페이스의 설정에서 최종적으로 생서된 state의 버전과 동일한 실행 버전인지 확인이 필요하다. (아래 캡쳐와 같이 State 버전이 설정의 Terraform Version 보다 높은 경우 에러 발생)
{
+ "version": 4,
+ "terraform_version": "0.13.7",
+ "serial": 3,
+ "lineage": "83b1b4d5-826a-ecc9-ce4a-ea41f06f93af",
+ "outputs": {
+ "uuid_from_0_13": {
+ "value": "34ee63e3-860b-1a1b-c0fa-1546f922be5a-0.12",
+ "type": "string"
+ }
+ },
+ "resources": [
+ {
+ "mode": "data",
+ "type": "terraform_remote_state",
+ "name": "v0_13",
+ "provider": "provider[\"terraform.io/builtin/terraform\"]",
+ "instances": [
+
+ ]
+ }
+ ]
+}
+
$ terraform version
+Terraform v0.12.31
+
관련 Knowledge Base Article : https://support.hashicorp.com/hc/en-us/articles/4409044739859-Container-ptfe-base-startup-failed
기존에 설치되어 있는 TFE를 백업받고, 업그레이드 작업을 하는 경우 반드시 enc_password를 저장 후 작업하시기 바랍니다. 그렇지 않은 경우, 아래와 같이 ptfe_base_startup failed 때문에 구성이 불가할 수 있습니다.
보통 Automated Install (silent install)을 하는 경우 Application Settings에 지정 후 설치하게 되어 해당 파일에 enc_password 값이 저장됩니다. (https://www.terraform.io/enterprise/install/automated/automating-the-installer#application-settings)
설치 시 사용한 TF 코드나 수작업을 생성한 settings.json 파일을 잘 보관하셔서 upgrade나 복구 시 이용할 수 있도록 준비하시는 게 나을 듯 합니다.
https://github.com/hashicorp/terraform/tree/main/tools/terraform-bundle
Terraform Enterprise에서 동작하는 기능입니다.
Airgap 환경에서 사용할 특정 버전의 Terraform과 여러 제공자 플러그인을 모두 포함하는 zip 파일 인 "번들 아카이브"를 생성하는 툴을 사용합니다. 일반적으로 Terraform init을 통해 특정 구성 작업에 필요한 플러그인을 다운로드하고 설치하지만 Airgap 환경에서는 공식 플러그인 저장소에 액세스 할 수 없는 경우가 발생합니다. Bundle 툴을 사용하여 Terraform 버전과 선택한 공급자를 모두 설치하기 위해 대상 시스템에 압축을 풀 수있는 zip 파일이 생성되므로 즉석 플러그인 설치가 필요하지 않습니다.
주의
번들로 작성된 zip파일을 url로 등록하기 때문에 번들을 다운받을 수 있는 웹서버나 넥서스 같은 원격 저장소가 필요합니다.
terraform-bundle
을 빌드합니다.# Ubuntu
+# Install Go: https://github.com/golang/go/wiki/Ubuntu
+$ sudo add-apt-repository ppa:longsleep/golang-backports
+$ sudo apt update -y
+$ sudo apt install golang-go -y
+#sudo apt install golang-1.14-go -y
+
+# Build terraform-bundle from a release tag that matches your TF version
+# Otherwise you might get an error like:
+# "Failed to read config: this version of terraform-bundle can only build bundles for . . ."
+$ git clone https://github.com/hashicorp/terraform.git
+$ cd terraform
+$ go install ./tools/terraform-bundle
+
+#verify that terraform-bundle tool is there
+$ ls ~/go/bin/
+$ export PATH=${PATH}:$HOME/go/bin/
+$ terraform-bundle --version
+0.13.0
+
bundle 구성할 명세를 hcl로 작성합니다. (e.g. tf-bundle.hcl)
팁
공식(Official) 프로바이더의 경우 source
정의를 생략할 수 있습니다. 그렇지 않는 경우에는 반드시 source
에 대한 정의가 필요합니다.
terraform {
+ # Version of Terraform to include in the bundle. An exact version number is required.
+ version = "0.15.4"
+}
+
+# Define which provider plugins are to be included
+providers {
+ null = {
+ versions = ["= 3.1.0"]
+ }
+ time = {
+ versions = ["= 0.7.1"]
+ }
+ random = {
+ versions = ["= 3.1.0"]
+ }
+ template = {
+ versions = ["= 2.2.0"]
+ }
+ tfe = {
+ versions = ["= 0.25.3"]
+ }
+ vsphere = {
+ versions = ["= 1.26.0"]
+ }
+ vault = {
+ versions = ["= 2.20.0"]
+ }
+ consul = {
+ versions = ["= 2.12.0"]
+ }
+ kubernetes = {
+ versions = ["= 2.2.0"]
+ }
+ ad = {
+ versions = ["=0.4.2"]
+ }
+ openstack = {
+ versions = ["= 1.42.0"]
+ source = "terraform-provider-openstack/openstack"
+ }
+ nsxt = {
+ versions = ["= 3.1.1"]
+ source = "vmware/nsxt"
+ }
+ vra7 = {
+ versions = ["= 3.0.2"]
+ source = "vmware/vra7"
+ }
+}
+
번들 생성은 다음과 같이 커맨드로 실행 합니다.
terraform-bundle package -os=linux -arch=amd64 tf-bundle.hcl
+
이 작업을 통해 Terraform Enterprise에서 기존 Terraform을 다운로드 받고 Provider를 다운로드 받던 동작을 미리 수행한 번들이 생성 됩니다.
생성된 번들 파일(zip)은 TFE Admin Console을 통해 적용
shasum -a256 ./terraform_0.15.4-bundle2021060202_linux_amd64.zip
https://www.terraform.io/docs/cli/config/config-file.html#implied-local-mirror-directories
https://learn.hashicorp.com/tutorials/terraform/provider-use?in=terraform/providers
OS : CentOS7
NAME="CentOS Linux"
+VERSION="7 (Core)"
+ID="centos"
+ID_LIKE="rhel fedora"
+VERSION_ID="7"
+PRETTY_NAME="CentOS Linux 7 (Core)"
+ANSI_COLOR="0;31"
+CPE_NAME="cpe:/o:centos:centos:7"
+HOME_URL="https://www.centos.org/"
+BUG_REPORT_URL="https://bugs.centos.org/"
+
+CENTOS_MANTISBT_PROJECT="CentOS-7"
+CENTOS_MANTISBT_PROJECT_VERSION="7"
+REDHAT_SUPPORT_PRODUCT="centos"
+REDHAT_SUPPORT_PRODUCT_VERSION="7"
+
Terraform
$ terraform version
+Terraform v1.0.0
+
필요한 Provider zip파일을 https://releases.hashicorp.com 에서 미리 다운 받습니다. 받아놓은 zip 파일이 있는 경우 대상 시스템에 복사해둡니다.
$ wget https://releases.hashicorp.com/terraform-provider-nsxt/3.2.1/terraform-provider-nsxt_3.2.1_linux_amd64.zip
+$ wget https://releases.hashicorp.com/terraform-provider-random/3.1.0/terraform-provider-random_3.1.0_linux_amd64.zip
+
Plugin 디렉토리 구성
로컬 Provider를 찾기위한 디렉토리 구조를 생성합니다. host_name
은 환경마다 상이할 수 있습니다.
~/.terraform.d/plugins/${host_name}/${namespace}/${type}/${version}/${target}
$ mkdir -p ~/.terraform.d/plugins/localhost.localdomain/vmware/nsxt/3.2.1/linux_amd64
+$ mkdir -p ~/.terraform.d/plugins/localhost.localdomain/hashicorp/random/3.1.0/linux_amd64
+
Provider 바이너리 파일 구성
기존에 받아놓은 zip 파일을 압축 해제하고, 생성한 Provider 디렉토리 각각에 맞는 프로바이더를 복사합니다.
$ unzip terraform-provider-random_3.1.0_linux_amd64.zip
+Archive: terraform-provider-random_3.1.0_linux_amd64.zip
+ inflating: terraform-provider-random_v3.1.0_x5
+
+$ mv ./terraform-provider-random_v3.1.0_x5 ~/.terraform.d/plugins/localhost.localdomain/hashicorp/random/3.1.0/linux_amd64
+
+$ unzip terraform-provider-nsxt_3.2.1_linux_amd64.zip
+Archive: terraform-provider-nsxt_3.2.1_linux_amd64.zip
+ inflating: CHANGELOG.md
+ inflating: LICENSE.txt
+ inflating: README.md
+ inflating: terraform-provider-nsxt_v3.2.1
+
+$ mv ./terraform-provider-nsxt_v3.2.1 ~/.terraform.d/plugins/localhost.localdomain/vmware/nsxt/3.2.1/linux_amd64
+
로컬 Provider 구성 확인
파일 구조
$ tree -a ~/.terraform.d/
+/root/.terraform.d/
+├── `plugins`
+│ └── localhost.localdomain
+│ ├── hashicorp
+│ │ └── random
+│ │ └── 3.1.0
+│ │ └── linux_amd64
+│ │ └── terraform-provider-random_v3.1.0_x5
+│ └── vmware
+│ └── nsxt
+│ └── 3.2.1
+│ └── linux_amd64
+│ └── terraform-provider-nsxt_v3.2.1
+├── checkpoint_cache
+└── checkpoint_signature
+
워크스페이스 생성 (디렉토리) - airgapped 는 임의의 이름 입니다.
$ mkdir ./airgapped
+$ cd ./airgapped
+
tf 파일 작성
$ cat <<EOF> terraform.tf
+terraform {
+ required_providers {
+ nsxt = {
+ source = "localhost.localdomain/vmware/nsxt"
+ version = "3.2.1"
+ }
+ random = {
+ source = "localhost.localdomain/hashicorp/random"
+ version = "3.1.0"
+ }
+ }
+}
+
+provider "nsxt" {
+ # Configuration options
+}
+
+provider "random" {
+ # Configuration options
+}
+
+resource "random_id" "test" {
+ byte_length = 8
+}
+
+output "random_id" {
+ value = random_id.test
+}
+EOF
+
Terraform init
을 수행하여 정상적으로 로컬 Provider를 가져오는지 확인합니다.
$ terraform init
+
+Initializing the backend...
+
+Initializing provider plugins...
+- Finding localhost.localdomain/vmware/nsxt versions matching "3.2.1"...
+- Finding localhost.localdomain/hashicorp/random versions matching "3.1.0"...
+- Installing localhost.localdomain/vmware/nsxt v3.2.1...
+- Installed localhost.localdomain/vmware/nsxt v3.2.1 (unauthenticated)
+- Installing localhost.localdomain/hashicorp/random v3.1.0...
+- Installed localhost.localdomain/hashicorp/random v3.1.0 (unauthenticated)
+
+Terraform has created a lock file .terraform.lock.hcl to record the provider
+selections it made above. Include this file in your version control repository
+so that Terraform can guarantee to make the same selections by default when
+you run "terraform init" in the future.
+
+Terraform has been successfully initialized!
+
+You may now begin working with Terraform. Try running "terraform plan" to see
+any changes that are required for your infrastructure. All Terraform commands
+should now work.
+
+If you ever set or change modules or backend configuration for Terraform,
+rerun this command to reinitialize your working directory. If you forget, other
+commands will detect it and remind you to do so if necessary.
+
https://www.terraform.io/docs/cli/config/config-file.html#provider_installation
Terraform CLI를 사용할 때, 기본적으로 코드 상에서 사용하는 플러그인은 registry.terraform.io에서 다운로드 받게 되어 있습니다.
하지만 네트워크이 느리거나 폐쇄망인 경우, 직접 다운로드가 아닌 다른 방법으로 프로바이더를 사용할 수 있습니다.
CLI 설정 파일에 명시적으로 설정하는 방법과 설정하지 않고 사용하는 방법이 있습니다.
상대적으로 설정이 간편한 filesystem_mirror 설정 방법은 다음과 같습니다.
Terraform 사용 환경에 맞춰 terraform configuration 파일 구성하기
다음 처럼 'provider_installation' 설정하기
provider_installation {
+ filesystem_mirror {
+ path = "/usr/share/terraform/providers"
+ include = ["*/*"] # registry.terrafom.io/hashicorp/*
+ }
+}
+
대상 디렉토리 설정하기
예를 들어 aws provider는 다음과 같이 코드 상에 사용
terraform {
+ required_providers {
+ aws = {
+ source = "hashicorp/aws"
+ version = "3.36.0"
+ }
+ }
+}
+
지정된 경로 상에 다음과 같은 HOSTNAME/NAMESPACE/TYPE/VERSION/TARGET 형태로 디렉토리 구조를 지정
사용하시고자 하는 프로바이어더의 다운로드는 다음 링크에서 가능합니다. https://releases.hashicorp.com
하나의 Consul 서버에서 서로다른 Agent 들의 묶음을 관리할 수 있도록 하는 개념입니다. 여러개의 Consul 클러스터를 만드는 것이 아닌, 하나의 클러스터 내에서 Agent 들을 분류합니다.
참고 Url : https://learn.hashicorp.com/tutorials/consul/network-partition-datacenters
페더레이션을 위해서는 RPC(8300/tcp), SerfWAN(8302/tcp, 8302/udp)를 통해야하므로 여러 테이터센터, 혹은 클러스터를 관리하기에 어려워집니다. 이를 보완하기 위해 지역간 트래픽을 모두 RPC(8300/tcp)를 통해 수행하여 TLS만으로 보안을 유지하도록 합니다.
참고 Url : https://www.consul.io/docs/enterprise/federation
일반적으로 클러스터의 관리 서버는 3중화하여 구성하며 Raft 알고리즘을 통해 리더 선출을 하는 방식을 취합니다. HA를 위해 해당 투표에 참여하지 않은 추가 잉여 서버 노드를 "상시 대기" 시킬 수 있으며 장애 발생 시 해당 노드는 투표 구성원으로 승격 됩니다. 이는 서버 노드에 대한 추가 구성원임과 동시에 복구 기능을 제공합니다.
참고 Url : https://learn.hashicorp.com/tutorials/consul/autopilot-datacenter-operations#redundancy-zones
앞서의 Redendancy Zone의 비투표 노드는 투표에는 참여하지 않지만 데이터를 복제하고 데이터를 읽을 수 있는 동작을 지원합니다. 이를 통해 Consul 클러스터를 확장할 수 있고 읽기/쓰기 지연시간을 줄이고 용량을 늘일 수 있습니다.
참고 Url : https://www.consul.io/docs/agent/options#_non_voting_server
관련 블로그 : https://www.hashicorp.com/blog/enterprise-compliance-and-governance-with-hashicorp-consul-1-8/
Namespaces 기능은 사용자나 팀간의 데이터 격리를 제공합니다. 각각의 Namespace로 구분된 서비스와 정보는 서로 다른 구성원같에 조회가 불가능합니다. 하나의 클러스터로 논리적 분할을 가능하게 합니다.
참고 Url : https://www.consul.io/docs/enterprise/namespaces
Open ID Connect를 사용하여 인증을 처리합니다.
참고 Url : https://www.consul.io/docs/acl/auth-methods/oidc
사용자의 이벤트 시도와 처리에 대한 로그를 캡쳐하고 기록하는 것을 지원합니다. 1.8.0 기준으로 'file'을 지원하며, 향후 추가 타입이 지원될 예정입니다.
참고 Url : https://www.consul.io/docs/agent/options#audit
Consul에 반영되는 서비스나 KV에 대한 정책을 정의할 수 있습니다. 예를 들면 서비스 등록이나 업데이트에 대한 시간을 강제하거나 Key에 이름 패턴을 강제화 할 수 있습니다.
https://learn.hashicorp.com/tutorials/consul/reference-architecture
Consul은 Server/Client 구조로 구성되며, Client의 경우 자원사용량이 매우 미미하므로 자원산정은 Server를 기준으로 산정
Size | CPU | Memory | Disk Capacity | Disk IO | Disk Throughput |
---|---|---|---|---|---|
최소 | 2-4 core | 8-16 GB RAM | 100 GB | 3000+ IOPS | 75+ MB/s |
권장 | 8-16 core | 32-64 GB RAM | 100 GB | 7500+ IOPS | 250+ MB/s |
Provider | Size | Instance/VM Types | Disk Volume Specs |
---|---|---|---|
AWS | 최소 | m5.large , m5.xlarge | 100+GB gp3 , 3000 IOPS, 125MB/s |
권장 | m5.2xlarge , m5.4xlarge | 200+GB gp3 , 10000 IOPS, 250MB/s | |
Azure | 최소 | Standard_D2s_v3 , Standard_D4s_v3 | 1024GB* Premium SSD , 5000 IOPS, 200MB/s |
권장 | Standard_D8s_v3 , Standard_D16s_v3 | 2048GB* Premium SSD , 7500 IOPS, 200MB/s | |
GCP | 최소 | n2-standard-2 , n2-standard-4 | 512GB* pd-balanced , 15000 IOPS, 240MB/s |
권장 | n2-standard-8 , n2-standard-16 | 1000GB* pd-ssd , 30000 IOPS, 480MB/s |
Use | Default Ports |
---|---|
DNS (TCP and UDP) | 8600 |
HTTP (TCP Only) | 8500 |
HTTPS (TCP Only) | disabled (8501)* |
gRPC (TCP Only) | disabled (8502)* |
LAN Serf (TCP and UDP) | 8301 |
Wan Serf (TCP and UDP) | 8302 |
server (TCP Only) | 8300 |
Sidecar Proxy Min: 자동으로 할당된 사이드카 서비스 등록에 사용할 포함 최소 포트 번호 | 21000 |
Sidecar Proxy Max: 자동으로 할당된 사이드카 서비스 등록에 사용할 포괄적인 최대 포트 번호 | 21255 |
#systemd-resolved 설정파일 추가 및 변경
+mkdir -p /etc/systemd/resolved.conf.d
+(
+cat <<-EOF
+[Resolve]
+DNS=127.0.0.1
+DNSSEC=false
+Domains=~consul
+EOF
+) | sudo tee /etc/systemd/resolved.conf.d/consul.conf
+(
+cat <<-EOF
+nameserver 127.0.0.1
+options edns0 trust-ad
+EOF
+) | sudo tee /etc/resolv.conf
+#iptables에 consul dns port 추가
+iptables --table nat --append OUTPUT --destination localhost --protocol udp --match udp --dport 53 --jump REDIRECT --to-ports 8600
+iptables --table nat --append OUTPUT --destination localhost --protocol tcp --match tcp --dport 53 --jump REDIRECT --to-ports 8600
+#service 재시작
+systemctl restart systemd-resolved
+
#Global domain에 consul 확인
+$ resolvectl domain
+Global: ~consul
+Link 5 (docker0):
+Link 4 (eth2):
+Link 3 (eth1):
+Link 2 (eth0):
+#consul service확인, 해당 클러스터에는 consul server가 3대임
+$ resolvectl query consul.service.consul
+consul.service.consul: 172.30.1.100
+ 172.30.1.101
+ 172.30.1.102
+
+
Consul ACL을 활성화 할 경우 default를 deny로 할 지 allow를 할 지 정할 수 있다.
deny로 할 경우에는 하나하나 policy로 tokne을 만들어서 사용해야 한다.
key_prefix "vault/" {
+ policy = "write"
+}
+service "vault" {
+ policy = "write"
+}
+agent_prefix "" {
+ policy = "read"
+}
+session_prefix "" {
+ policy = "write"
+}
+
node_prefix "" {
+ policy = "read"
+}
+service_prefix "" {
+ policy = "read"
+}
+# only needed if using prepared queries
+query_prefix "" {
+ policy = "read"
+}
+
service_prefix "" {
+ policy = "read"
+}
+key_prefix "" {
+ policy = "read"
+}
+node_prefix "" {
+ policy = "read"
+}
+
팁
최대한 설정값을 넣어보고, 번역기도 돌려보고 물어도 보고 넣은 Client설정 파일입니다.
네트워크는 프라이빗(온프레이머스) 환경입니다.
#consul client 설정
+server = false
+
+acl = {
+ enabled = true
+ default_policy = "deny"
+ enable_token_persistence = true
+ tokens = {
+ agent = "f820514a-5215-e741-fcb3-c00857405230"
+ }
+}
+
+license_path = "/opt/license/consul.license"
+
+retry_join = ["172.30.1.17","172.30.1.18","172.30.1.19"]
+
+rejoin_after_leave = true
+
+
+#tls 설정
+ca_file = "/opt/ssl/consul/consul-agent-ca.pem"
+auto_encrypt = {
+ tls = true
+}
+
+verify_incoming = false
+verify_outgoing = true
+verify_server_hostname = true
+
data_dir = "/opt/consul"
+
+client_addr = "0.0.0.0"
+
+datacenter = "my-dc"
+
+# client
+server = false
+
+# Bind addr
+bind_addr = "0.0.0.0" # Listen on all IPv4
+# Advertise addr - if you want to point clients to a different address than bind or LB.
+advertise_addr = "node ip"
+
+# Enterprise License
+license_path = "/opt/consul/consul.lic"
+
+# encrypt
+encrypt = "7w+zkhqa+YD4GSKXjRWETBIT8hs53Sr/w95oiVxq5Qc="
+
+# retry_join
+retry_join = ["server ip"]
+
+ca_file = "/opt/consul/consul-agent-ca.pem"
+cert_file = "/opt/consul/my-dc-client-consul-0.pem"
+key_file = "/opt/consul/my-dc-client-consul-0-key.pem"
+
+verify_incoming = false
+verify_incoming_rpc = false
+verify_outgoing = false
+verify_server_hostname = false
+
+ports {
+ http = 8500
+ dns = 8600
+ server = 8300
+}
+
팁
최대한 설정값을 넣어보고, 번역기도 돌려보고 물어도 보고 넣은 server, client의 공통설정 파일입니다.
저는 agent.hcl파일안에 다 넣고 실행하지만 나눠서 추후에는 기능별로 나눠서 사용할 예정입니다.
#node name에는 _금지
+#node_name
+
+client_addr = "0.0.0.0"
+bind_addr = "{{ GetInterfaceIP `ens192` }}"
+advertise_addr = "{{ GetInterfaceIP `ens224` }}"
+
+#ipv4, ipv6를 나눠서 설정할 수 있음.
+#advertise_addr_ipv4
+#advertise_addr_ipv6
+
+ports {
+ #http = 8500
+ http = -1
+ dns = 8600
+ #https = -1
+ https = 8500
+ serf_lan = 8301
+ grpc = 8502
+ server = 8300
+}
+
+#gossip ip 지정
+#serf_lan
+#gossip 대역대 지정
+#serf_lan_allowed_cidrs
+
+#사용자 감사, 사용자가 consul에서 사용한 행동을 기록
+#audit {
+# enabled = true
+# sink "My sink" {
+# type = "file"
+# format = "json"
+# path = "data/audit/audit.json"
+# #consul의 감사작성방법 규칙, 현재는 best-effort만지원
+# delivery_guarantee = "best-effort"
+# rotate_duration = "24h"
+# rotate_max_files = 15
+# rotate_bytes = 25165824
+# }
+#}
+
+#consul 서버관리 설정 변경
+#autopoilt {
+# #새로운 서버가 클러스터에 추가될 때 죽은 서버 자동제거
+# cleanup_dead_servers = ture
+#
+# last_contact_threshold = 200ms
+# #최소 quorm 수 지정
+# min_quorum = ni
+# #클러스터에 서버가 추가될 시 안정상태로 되어야 하는 최소 시간
+# server_stabilization_time = 10s
+#}
+
+#동시에 처리할 수 있는 인증서 서명 요청 제한
+#csr_max_concurrent = 0
+#서버가 수락할 인증서 서명 요청(CSR)의 최대 수에 대한 속도 제한을 설정
+#csr_max_per_second = 50
+#클러스터에서 이전 루트 인증서를 교체할 때 사용
+#leaf_cert_ttl = 72h
+#CA 키 생성 타입
+#private_key_type = ec
+#CA 키 생성될 길이
+#private_key_bits = 256
+
+#서버에서만 client를 join할 수 있게 함
+#disable remote exec
+
+#enable syslog = true
+log_level = "DEBUG"
+data_dir = "/var/log/consul/consul"
+log_file = "/var/log/consul/consul.log"
+log_rotate_duration = "24h"
+log_rotate_bytes = 104857600
+log_rotate_max_files = 100
+
+license_path = "/opt/license/consul.license"
+
+acl {
+ enabled = true
+ default_policy = "allow"
+ enable_token_persistence = true
+
+ #acl policy ttl, 줄이면 새로고침 빈도 상승, 성능에 영향을 미칠 수 있음
+ #policy_ttl = 30s
+ #acl role ttl, 줄이면 새로고침 빈도 상승, 성능에 영향을 미칠 수 있음
+ #role_ttl = 30s
+}
+
+connect {
+ enabled = true
+ #vault 연동 옵션
+ #ca_provider
+}
+
+dns_config {
+ allow_stale = true,
+ max_stale = "87600h"
+}
+
+#block_endpoints할성화시 restapi 차단
+#http_config {
+# block_endpoints = false
+#}
+
+#segments
+
+rpc {
+ enable_streaming = true
+}
+
+encrypt = "7VY2fVm0p6vJUYNS/oex/mr2e59dy4AaGMefTKtUGi0="
+encrypt_verify_incoming = false
+encrypt_verify_outgoing = false
+
팁
최대한 설정값을 넣어보고, 번역기도 돌려보고 물어도 보고 넣은 server설정 파일입니다.
네트워크는 프라이빗(온프레이머스) 환경입니다.
#consul server 설정
+server = true
+ui_config {
+ enabled = true
+}
+bootstrap_expect = 3
+
+license_path = "/opt/license/consul.license"
+
+retry_join = ["172.30.1.17","172.30.1.18","172.30.1.19"]
+
+performance {
+ raft_multiplier = 1
+}
+
+#raft protocal 버전, consul 업데이트 시 1씩 증가
+raft_protocol = 3
+
+#node가 완전히 삭제되는 시간
+reconnect_timeout = "72h"
+
+raft_snapshot_interval = "5s"
+
+#해당 서버를 non-voting server로 지정
+#read_replica = false
+
+limits {
+ http_max_conns_per_client = 200
+ rpc_handshake_timeout = "5s"
+}
+
+key_file = "/opt/ssl/consul/dc1-server-consul-0-key.pem"
+cert_file = "/opt/ssl/consul/dc1-server-consul-0.pem"
+ca_file = "/opt/ssl/consul/consul-agent-ca.pem"
+auto_encrypt {
+ allow_tls = true
+}
+
+verify_incoming = false,
+verify_incoming_rpc = true
+verify_outgoing = true
+verify_server_hostname = false
+
팁
최소한의 설정만 있는 consul 설정입니다.
data_dir = "/opt/consul"
+
+client_addr = "0.0.0.0"
+
+datacenter = "my-dc"
+
+#ui
+ui_config {
+ enabled = true
+}
+
+# server
+server = true
+
+# Bind addr
+bind_addr = "0.0.0.0" # Listen on all IPv4
+# Advertise addr - if you want to point clients to a different address than bind or LB.
+advertise_addr = "node ip"
+
+# Enterprise License
+license_path = "/opt/consul/consul.lic"
+
+# bootstrap_expect
+bootstrap_expect=1
+
+# encrypt
+encrypt = "7w+zkhqa+YD4GSKXjRWETBIT8hs53Sr/w95oiVxq5Qc="
+
+# retry_join
+retry_join = ["Server ip"]
+
+key_file = "/opt/consul/my-dc-server-consul-0-key.pem"
+cert_file = "/opt/consul/my-dc-server-consul-0.pem"
+ca_file = "/opt/consul/consul-agent-ca.pem"
+auto_encrypt {
+ allow_tls = true
+}
+
+verify_incoming = false
+verify_incoming_rpc = false
+verify_outgoing = false
+verify_server_hostname = false
+
+ports {
+ http = 8500
+ dns = 8600
+ server = 8300
+}
+
+
+
이 문서에서는 Consul을 사용하여 상이한 두 Consul로 구성된 클러스터(마스터가 별개)의 서비스를 연계하는 방법을 설명합니다.
네트워크 영역이 분리되어있는 두 환경의 애플리케이션 서비스들을 Service Mesh로 구성하는 방법을 알아 봅니다. 이번 구성 예에서는 Kubernetes와 Baremetal(BM)이나 VirtualMachine(VM)에 Consul Cluster(Datacenter)를 구성하고 각 환경의 애플리케이션 서비스를 Mesh Gateway로 연계합니다.
Mesh Gateway를 사용하면 서로다른 클러스터간에 mTLS 환경의 통신과 서비스 간의 트래픽 통로를 단일화 하여 구성할 수 있습니다. 또한 mTLS내의 데이터가 Gateway에서 해동되지 않기 때문에 두 클러스터간 안전하게 데이터를 송수신 합니다.
Consul의 각 Cluster는 Datacenter라는 명칭으로 구분됩니다. 이번 구성에서는 Kubernetes의 Consul Datacenter가 Primary의 역할을 합니다.
Port 구성에 대한 문서는 다음을 참고합니다.
https://www.consul.io/docs/install/ports
Use | Default Ports | CLI |
---|---|---|
DNS: The DNS server (TCP and UDP) | 8600 | -dns-port |
HTTP: The HTTP API (TCP Only) | 8500 | -http-port |
HTTPS: The HTTPs API | disabled (8501)* | -https-port |
gRPC: The gRPC API | disabled (8502)* | -grpc-port |
LAN Serf: The Serf LAN port (TCP and UDP) | 8301 | -serf-lan-port |
Wan Serf: The Serf WAN port (TCP and UDP) | 8302 | -sert-wan-port |
server: Server RPC address (TCP Only) | 8300 | -server-port |
Sidecar Proxy Min: Sidecar 서비스 등록에 사용되는 범위의 최소 포트 | 21000 | Configration file |
Sidecar Proxy Max: Sidecar 서비스 등록에 사용되는 범위의 최대 포트 | 21255 | Configration file |
Federation을 위한 포트로는
각 포트 구성설정 가이드는 다음과 같습니다.
ports {
+ dns = 8600
+ http = 8500
+ https = -1
+ grpc = -1
+ serf_lan = 8301
+ serf_wan = 8302
+ server = 8300
+ sidecar_min_port = 21000
+ sidecar_max_port = 21255
+ expose_min_port = 21500
+ expose_max_port = 21755
+}
+
Kubernetes에서 Consul을 실행하는 권장 방법은 Helm 차트를 사용하는 것 입니다. Consul을 실행하는 데 필요한 모든 구성 요소를 설치하고 구성합니다. Helm 2를 사용하는 경우 Helm 2 설치 가이드 에 따라 Tiller를 설치해야합니다.
HashiCorp helm Repository를 추가합니다.
$ helm repo add hashicorp https://helm.releases.hashicorp.com
+"hashicorp" has been added to your repositories
+
Consul 차트에 접근가능한지 확인합니다.
$ helm search repo hashicorp/consul
+NAME CHART VERSION APP VERSION DESCRIPTION
+hashicorp/consul 0.32.1 1.10.0 Official HashiCorp Consul Chart
+
Consul 차트마다의 기본 매칭되는 버전정보는 다음과 같이 리스트로 확인 가능합니다.
$ helm search repo hashicorp/consul -l
+NAME CHART VERSION APP VERSION DESCRIPTION
+hashicorp/consul 0.32.1 1.10.0 Official HashiCorp Consul Chart
+hashicorp/consul 0.32.0 1.10.0 Official HashiCorp Consul Chart
+hashicorp/consul 0.31.1 1.9.4 Official HashiCorp Consul Chart
+hashicorp/consul 0.31.0 1.9.4 Official HashiCorp Consul Chart
+hashicorp/consul 0.30.0 1.9.3 Official HashiCorp Consul Chart
+hashicorp/consul 0.29.0 1.9.2 Official HashiCorp Consul Chart
+hashicorp/consul 0.28.0 1.9.1 Official HashiCorp Consul Chart
+hashicorp/consul 0.27.0 1.9.0 Official HashiCorp Consul Chart
+hashicorp/consul 0.26.0 1.8.5 Official HashiCorp Consul Chart
+hashicorp/consul 0.25.0 1.8.4 Official HashiCorp Consul Chart
+hashicorp/consul 0.24.1 1.8.2 Official HashiCorp Consul Chart
+hashicorp/consul 0.24.0 1.8.1 Official HashiCorp Consul Chart
+hashicorp/consul 0.23.1 1.8.0 Official HashiCorp Consul Chart
+hashicorp/consul 0.23.0 1.8.0 Official HashiCorp Consul Chart
+hashicorp/consul 0.22.0 1.8.0 Official HashiCorp Consul Chart
+hashicorp/consul 0.21.0 1.7.3 Official HashiCorp Consul Chart
+hashicorp/consul 0.20.1 1.7.2 Official HashiCorp Consul Chart
+
Kubernetes상에서 Consul Datacenter의 Gossip 프로토콜에서 사용할 키를 생성합니다. 미리 생성하여 값을 넣어도 되고, 생성시 값이 생성되도록 하여도 관계 없습니다.
$ kubectl create secret generic consul-gossip-encryption-key --from-literal=key=$(consul keygen)
+
+--- or ---
+
+$ consul keygen
+h65lqS3w4x42KP+n4Hn9RtK84Rx7zP3WSahZSyD5i1o=
+$ kubectl create secret generic consul-gossip-encryption-key --from-literal=key=h65lqS3w4x42KP+n4Hn9RtK84Rx7zP3WSahZSyD5i1o=
+
yaml
작성Helm 차트로 설치할 때 기본 설정을 엎어쓰는 파일을 생성하여 원하는 구성으로 설치되도록 준비합니다. 각 구성에 대한 설정은 Helm Chart Configuration 를 참고합니다.
# consul.yaml
+global:
+ name: consul
+ # 기본이미지(OSS 최신 버전)가 아닌 다른 버전의 컨테이너 이미지 또는 별도의 레지스트리를 사용하는 경우 명시합니다.
+ image: 'hashicorp/consul-enterprise:1.8.5-ent'
+ datacenter: 'tsis-k8s'
+ tls:
+ # Federation 구성을 위해서는 TLS가 반드시 활성화되어야 합니다.
+ enabled: true
+ verify: false
+ httpsOnly: false
+
+ federation:
+ enabled: true
+ # Kubernetes가 Primary Datacenter이기 때문에 이 환경에서 Federation을 위한 시크릿을 생성하도록 합니다.
+ # https://www.consul.io/docs/k8s/installation/multi-cluster/kubernetes#primary-datacenter
+ createFederationSecret: true
+ gossipEncryption:
+ # gossip프로토콜은 암호화되어야 하며, 해당 키는 미리 Kubernetes에 Secret으로 구성합니다.
+ secretName: consul-gossip-encryption-key
+ secretKey: key
+ enableConsulNamespaces: true
+server:
+ enterpriseLicense:
+ secretName: consul-enterprise-license-key
+ secretKey: key
+connectInject:
+ enabled: true
+ centralConfig:
+ enabled: true
+ui:
+ service:
+ # UI에 접속을 위한 타입을 정의합니다.
+ # 보안상의 이유로 LoadBalancer기본적으로 서비스를 통해 노출되지 않으므로 kubectl port-forward를 사용하거나
+ # NodePort로 UI에 접속하는 데 사용해야 합니다.
+ type: NodePort
+dns:
+ enabled: true
+meshGateway:
+ # 메시 게이트웨이는 데이터 센터 간의 게이트웨이입니다.
+ # 데이터 센터 간의 통신이 메시 게이트웨이를 통과하므로 Kubernetes에서 페더레이션을 위해 활성화되어야합니다.
+ enabled: true
+ service:
+ type: NodePort
+ nodePort: 31001
+
+# Ingress Gateway는 Kubernets로 요청되는 주요 관문을 Consul에서 설정하고 Service Mesh기능을 활성화 합니다.
+# 이번 시나리오에서는 필수 설정이 아닙니다.
+ingressGateways:
+ enabled: true
+ gateways:
+ - name: ingress-gateway
+ service:
+ type: NodePort
+ ports:
+ - port: 31000
+ nodePort: 31000
+
Helm3을 사용하여 사용자 구성으로 Consul을 설치하려면 다음을 실행합니다. (사용자 구성 파일 : consul.yaml
)
$ helm install consul hashicorp/consul -f consul.yaml --debug
+
설치가 완료되고 얼마안있어 Pod를 확인해보면 다음과 같이 확인 가능합니다.
$ kubectl get pods
+consul-consul-mesh-gateway-754fbc5575-d8dgt 2/2 Running 0 2m
+consul-consul-mesh-gateway-754fbc5575-wkvjh 2/2 Running 0 2m
+consul-consul-mh5h6 1/1 Running 0 2m
+consul-consul-mx4mn 1/1 Running 0 2m
+consul-consul-rlb5x 1/1 Running 0 2m
+consul-consul-server-0 1/1 Running 0 2m
+consul-consul-server-1 1/1 Running 0 2m
+consul-consul-server-2 1/1 Running 0 2m
+consul-consul-tbngg 1/1 Running 1 2m
+consul-consul-tz9ct 1/1 Running 0 2m
+
port-forward
Consul UI에 접혹하기 위해 port-forward
를 사용하는 경우 다음과 같이 설정하여 접근가능합니다.
# HTTP
+$ kubectl port-forward service/consul-server 8500:8500
+
+# HTTPS (TLS)
+$ kubectl port-forward service/consul-server 8501:8501
+
ACL
ACL이 활성화된 경우 ACL토큰이 필요합니다. 전체 권한이 있는 bootstrap토큰은 다음과 같이 확인할 수 있습니다. (값의 마지막 %
는 제외)
$ kubectl get secrets/consul-bootstrap-acl-token --template={{.data.token}} | base64 -D
+e7924dd1-dc3f-f644-da54-81a73ba0a178%
+
Kubertnetes상에서 Mesh Gateway를 사용하기 위한 설정을 확인할 수 있도록 테스트를 위한 Pod를 생성합니다.
# k8s-consul-app.yaml
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: counting
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: counting
+ annotations:
+ 'consul.hashicorp.com/service-tags': servicemesh, consul, counting, v1
+ 'consul.hashicorp.com/connect-inject': 'true'
+spec:
+ containers:
+ - name: counting
+ image: hashicorp/counting-service:0.0.2
+ ports:
+ - containerPort: 9001
+ name: http
+ serviceAccountName: counting
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: dashboard
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: dashboard
+ labels:
+ app: 'dashboard'
+ annotations:
+ 'consul.hashicorp.com/service-tags': servicemesh, consul, dashiboard, v1
+ 'consul.hashicorp.com/connect-inject': 'true'
+ 'consul.hashicorp.com/connect-service-upstreams': 'counting:9001'
+spec:
+ containers:
+ - name: dashboard
+ image: hashicorp/dashboard-service:0.0.4
+ ports:
+ - containerPort: 9002
+ name: http
+ env:
+ - name: COUNTING_SERVICE_URL
+ value: 'http://localhost:9001'
+ serviceAccountName: dashboard
+---
+apiVersion: 'v1'
+kind: 'Service'
+metadata:
+ name: 'dashboard-service-nodeport'
+ namespace: 'default'
+ labels:
+ app: 'dashboard'
+spec:
+ ports:
+ - protocol: 'TCP'
+ port: 80
+ targetPort: 9002
+ selector:
+ app: 'dashboard'
+ type: 'NodePort'
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: dns
+spec:
+ containers:
+ - name: dns
+ image: anubhavmishra/tiny-tools
+ command:
+ - sleep
+ - infinity
+
'consul.hashicorp.com/connect-inject': 'true'
해당 annotations가 선언되면 consul api를 통해 해당 Pod가 배포될 때 Sidecar가 함께 생성됩니다.'consul.hashicorp.com/connect-service-upstreams': 'counting:9001'
설정은 side가에 9001포트로 요청이 오면 counting
으로 정의된 서비스로 해당 요청을 전달합니다.dashboard
앱은 frontend앱으로 UI를 제공하며, counting
앱은 backend앱으로 호출시 내부적으로 counting을 추가합니다.upstream
설정으로 9001에 대한 목적지를 알고 있는 sidecar container proxy가 해당 요청을 전달합니다.각 환경(Linux/Windows/Mac/FreeBSC/Solaris)에 맞는 바이너리를 받고 압축을 풉니다. consul
혹은 Windows의 경우 consul.exe
를 시스템의 적절한 위치에 이동시키고 PATH에 추가시키면 어느곳에서든 접근할 수 있습니다.
쉘 설정 파일을 편집하여 PATH에 영구적으로 추가할 수 있습니다. 일반적으로 .
+ 쉘이름
+ rc
로 구성되며 bash쉘을 사용하는 경우 ~/.bashrc
가 해당 파일입니다. 해당 파일에서 export PATH=
으로 시작하는 :(콜론)
으로 구분된 위치에 consul 바이너리 파일 위치를 넣어주거나 없는 경우 기존 PATH에 추가로 기입할 수 있습니다. /tools/consul_dir
디렉토리인경우 다음의 예와 같습니다.
...
+export PATH=$PATH:/tools/consul_dir
+
root 권한이 있다면 시스템의 기본 PATH로 지정되어있는 /usr/local/bin
디렉토리에 consul을 복사하는 것도 하나의 방법이 될 수 있습니다.
Windows
시스템 설정에서 GUI를 통해 PATH를 추가합니다. 마우스 클릭으로 진행하는 경우 Windows 설정 > 시스템 > 정보 > 시스템 정보 > 고급 시스템 설정 > 고급 탭 > 환경 변수
의 단계로 진행합니다. 작업표시줄의 검색창에서 고급 시스템 설정
을 검색하여 고급 탭 > 환경변수
로 이동할 수도 있습니다. 환경 변수 GUI에서 USER
또는 시스템 변수
의 Path에 Consul 디렉토리 경로를 추가합니다.
Federation Between VMs and Kubernetes : https://www.consul.io/docs/k8s/installation/multi-cluster/vms-and-kubernetes
Kubernetes에 구성된 Consul Datacenter가 Primary이기 때문에 해당 환경에서 TLS 인증서를 가져옵니다. 앞서 구성된 Kubernetes 환경에서 CA(Certificate authority cert)와 서명 키(Certificate Authority signing key)를 가져옵니다.
$ kubectl get secrets/consul-ca-cert \
+ --template='{{index .data "tls.crt" }}' | base64 -d > consul-agent-ca.pem
+$ kubectl get secrets/consul-ca-key \
+ --template='{{index .data "tls.key" }}' | base64 -d > consul-agent-ca-key.pem
+
두 파일이 생성된 위치에서 consul tls
명령을 사용하여 서버에서 사용할 인증서를 생성합니다.
$ consul tls cert create -server -dc=vm-dc
+==> Using consul-agent-ca.pem and consul-agent-ca-key.pem
+==> Saved vm-dc-server-consul-0.pem
+==> Saved vm-dc-server-consul-0-key.pem
+
동일한 위치에서 Client를 위한 인증서를 생성합니다.
$ consul tls cert create -client -dc=vm-dc
+==> Using consul-agent-ca.pem and consul-agent-ca-key.pem
+==> Saved vm-dc-client-consul-0.pem
+==> Saved vm-dc-client-consul-0-key.pem
+
CA 파일과 새로 생성한 파일을 Server와 Client 각 환경에 복사합니다. (e.g. /home/consul/consul-cert/vm-dc-server-consul-0.pem)
앞서 생성한 파일 이름을 기준으로 복사 대상은 다음과 같습니다.
CLI를 활용하여 Consul을 구동할 때 구성 옵션을 사용하는 것도 가능하나 여기서는 구성 파일을 작성하여 Consul Server나 Consul Client가 기동할 수 있도록 합니다. Server와 Client에 대한 설정에 약간의 차이가 있을 뿐 대부분이 동일합니다.
server = true
+ui = true
+bootstrap_expect = 3
+node_name = "consul_server_01"
+datacenter = "vm-dc"
+client_addr = "0.0.0.0"
+bind_addr = "192.168.100.51"
+encrypt = "h65lqS3w4x42KP+n4Hn9RtK84Rx7zP3WSahZSyD5i1o="
+data_dir = "/var/lib/consul"
+retry_join = ["192.168.100.51","192.168.100.52","192.168.100.83"]
+ports {
+ https = 8501
+ http = 8500
+ grpc = 8502
+}
+enable_central_service_config = true
+connect {
+ enabled = true
+ enable_mesh_gateway_wan_federation = true
+}
+primary_datacenter = "k8s-dc"
+primary_gateways = ["172.16.1.111:31001","172.16.1.116:31001"]
+cert_file = "/root/consul-cert/vm-dc-server-consul-0.pem"
+key_file = "/root/consul-cert/vm-dc-server-consul-0-key.pem"
+ca_file = "/root/consul-cert/consul-agent-ca.pem"
+
server : server로 구성되는 Consul의 경우에 true
로 설정합니다.
node_name, bind_addr는 각 Server에 맞게 구성합니다. 여기서는 3개의 서버로 구성하였습니다.
encrypt : consul keygen
을 생성한 값입니다. Server와 Client모두 동일한 값을 설정합니다.
data_dir : Consul의 데이터를 저장할 경로이며 미리 생성해야 합니다.
retry_join : Consul 서버의 IP를 기입합니다.
ports: Mesh Gateway구성을 위해 https를 활성화 합니다.
enable_central_service_config : federation 구성을 위해 true
로 설정합니다.
connect : Service Mesh 구성 활성화를 위해 구성합니다. enable_mesh_gateway_wan_federation
는 Federation에서 Mesh Gateway를 활성화 시켜줍니다.
primary_datacenter : kubernetes 환경의 Datacenter이름을 기입합니다.
primary_gateways : Kubernetes 환경의 Mesh Gateway 의 IP와 Port를 기입합니다. 여기 예제에서는 Nodeport로 구성된 Consul Mesh Gateway의 값이 확인됩니다.
$ kubectl exec statefulset/consul-server -- sh -c 'curl -sk https://localhost:8501/v1/catalog/service/mesh-gateway | jq ".[].ServiceTaggedAddresses.wan"'
+{
+ "Address": "172.16.1.111",
+ "Port": 31001
+}
+{
+ "Address": "172.16.1.116",
+ "Port": 31001
+}
+
cert_file / key_file / ca_file : 앞서 생성한 Server 인증서들의 경로와 파일명을 기입합니다.
node_name = "consul_client_01"
+datacenter = "vm-dc"
+client_addr = "0.0.0.0"
+bind_addr = "192.168.100.54"
+encrypt = "h65lqS3w4x42KP+n4Hn9RtK84Rx7zP3WSahZSyD5i1o="
+data_dir = "/var/lib/consul"
+retry_join = ["192.168.100.51","192.168.100.52","192.168.100.53"]
+cert_file = "/root/consul-cert/vm-dc-client-consul-0.pem"
+key_file = "/root/consul-cert/vm-dc-client-consul-0-key.pem"
+ca_file = "/root/consul-cert/consul-agent-ca.pem"
+
Linux 환경이나 Windows환경에서 서비스로 구성하면 시스템 부팅 시 자동으로 시작할 수 있기 때문에 선호되는 설치 방식 중 하나입니다. 이미 설치된 상태라면 앞서 구성을 변경하고 consul reload
를 사용하여 구성을 다시 읽어오거나 리스타트 합니다.
/etc/systemd/system/consul.service
에 다음의 서비스 파일을 작성합니다. 필요에 따라 User와 Group을 추가하여 구성하는 것도 가능합니다. 여기서는 consul User를 구성하여 사용하였습니다.
[Unit]
+Description=Consul Service Discovery Agent
+Documentation=https://www.consul.io/
+After=network-online.target
+Wants=network-online.target
+
+[Service]
+Type=simple
+User=consul
+Group=consul
+ExecStart=/usr/local/bin/consul agent -config-dir=/etc/consul.d
+
+ExecReload=/bin/kill -HUP $MAINPID
+KillSignal=SIGINT
+TimeoutStopSec=5
+Restart=on-failure
+SyslogIdentifier=consul
+
+[Install]
+WantedBy=multi-user.target
+
등록된 서비스를 활성화 하고 시작하여 상대를 확인합니다.
$ systemctl enable consul
+$ systemctl start consul
+$ systemctl status consul
+● consul.service - Consul Service Discovery Agent
+ Loaded: loaded (/etc/systemd/system/consul.service; enabled; vendor preset: disabled)
+ Active: active (running) since 토 2020-11-14 19:05:39 UTC; 17h ago
+ Docs: https://www.consul.io/
+ Main PID: 1020 (consul)
+ CGroup: /system.slice/consul.service
+ └─1020 /usr/local/bin/consul agent -config-dir=/etc/consul.d
+
+$ journalctl -u consul -f
+11월 15 13:06:12 cl01-consul-vault-0 consul[1020]: 2020-11-15T13:06:12.265Z [INFO] agent.server: Handled event for server in area: event=member-join server=consul-consul-server-1.tsis-k8s area=wan
+11월 15 13:06:18 cl01-consul-vault-0 consul[1020]: 2020-11-15T13:06:18.119Z [INFO] agent.server.memberlist.wan: memberlist: Suspect consul-consul-server-0.tsis-k8s has failed, no acks received
+
Powershell을 활용하여 서비스를 구성합니다.
> sc.exe create "Consul" binPath= "consul agent -config-dir=C:\ProgramData\consul\config" start= auto
+[SC] CreateService SUCCESS
+
+> sc.exe start "Consul"
+
+SERVICE_NAME: Consul
+ TYPE : 10 WIN32_OWN_PROCESS
+ STATE : 4 RUNNING (STOPPABLE, NOT_PAUSABLE, ACCEPTS_SHUTDOWN)
+ WIN32_EXIT_CODE : 0 (0x0)
+ SERVICE_EXIT_CODE : 0 (0x0)
+ CHECKPOINT : 0x0
+ WAIT_HINT : 0x0
+ PID : 8008
+ FLAGS :
+
Secondary Datacenter인 BM/VM 환경에서 primary_datacenter
를 지정하였기 때문에 기동 후 Kubernetes의 Consul과 Join되어 Federation이 구성됩니다.
Mesh Gateway를 구성하여 Service Mesh 환경이 멀티/하이브리드 Datacenter 환경을 지원하도록 합니다. Mesh Gateway는 Consul의 내장 Proxy로는 동작하지 못하므로 Envoy 를 설치하여 이를 활용합니다.
Consul의 각 버전별 지원하는 Envoy 버전은 다음 표와 같습니다.
Consul Version | Compatible Envoy Versions |
---|---|
1.10.x | 1.18.3, 1.17.3, 1.16.4, 1.15.5 |
1.9.x | 1.16.0, 1.15.2, 1.14.5‡, 1.13.6‡ |
1.8.x | 1.14.5, 1.13.6, 1.12.7, 1.11.2 |
1.7.x | 1.13.6, 1.12.7, 1.11.2, 1.10.0* |
1.6.x, 1.5.3, 1.5.2 | 1.11.1, 1.10.0, 1.9.1, 1.8.0† |
1.5.1, 1.5.0 | 1.9.1, 1.8.0† |
1.4.x, 1.3.x | 1.9.1, 1.8.0†, 1.7.0† |
경고
‡ Consul 1.9.x는 1.15.0+의 Envoy를 권장합니다.
† 1.9.1 버전 이하의 Envoy는 CVE-2019-9900, CVE-2019-9901 취약점이 보고되었습니다.
* Consul 1.7.x에서 Envoy 1.10.0을 사용하는 경우 consul connect envoy
커맨드 사용시 -envoy-version
옵션을 포함해야합니다.
Envoy 웹사이트 에서 직접 Envoy의 컨테이너 기반 빌드를 얻거나 func-e.io 와 같은 3rd party 프로젝트에서 Envoy 바이너리 빌드 패키지를 얻을 수 있습니다.
다음 명령을 실행하여 Envoy를 가져와 설치하는 func-e
유틸리티를 다운로드하고 설치합니다.
curl -L https://func-e.io/install.sh | bash -s -- -b /usr/local/bin
+
다음과 같이 대상 환경을 지정할 수 있습니다.
export FUNC_E_PLATFORM=darwin/amd64
+
go
out of the boxaix/ppc64
darwin/386
darwin/amd64
dragonfly/amd64
freebsd/386
freebsd/amd64
freebsd/arm
freebsd/arm64
illumos/amd64
js/wasm
linux/386
linux/amd64
linux/arm
linux/arm64
linux/ppc64
linux/ppc64le
linux/mips
linux/mipsle
linux/mips64
linux/mips64le
linux/riscv64
linux/s390x
netbsd/386
netbsd/amd64
netbsd/arm
netbsd/arm64
openbsd/386
openbsd/amd64
openbsd/arm
openbsd/arm64
plan9/386
plan9/amd64
plan9/arm
solaris/amd64
windows/386
windows/amd64
windows/arm
go
out of the boxdarwin/386
freebsd/386
freebsd/arm
linux/386
linux/arm
linux/mips
linux/mipsle
netbsd/386
netbsd/arm
openbsd/386
openbsd/arm
plan9/386
plan9/arm
windows/386
windows/arm
go
out of the boxaix/ppc64
darwin/amd64
dragonfly/amd64
freebsd/amd64
freebsd/arm64
illumos/amd64
js/wasm
linux/amd64
linux/arm64
linux/ppc64
linux/ppc64le
linux/mips64
linux/mips64le
linux/riscv64
linux/s390x
netbsd/amd64
netbsd/arm64
openbsd/amd64
openbsd/arm64
plan9/amd64
solaris/amd64
windows/amd64
특정 버전을 명시하여 다운로드 하려면 다음 명령을 실행합니다.
func-e use 1.18.3
+
Envoy 바이너리를 $PATH
의 위치에 복사합니다. 이를 통해 Consul은 바이너리 위치를 지정하지 않고 Envoy를 자동으로 시작할 수 있습니다.
sudo cp ~/.func-e/versions/1.18.3/bin/envoy /usr/local/bin/
+
다음 명령을 실행하여 Envoy가 $PATH
에 있는지 확인합니다.
envoy --version
+
Mesh Gateway는 TLS를 필요로하며 Consul과도 TLS로 통신 합니다. 따라서 Consul로의 기본 접속 방식과 포트를 SSL기준으로 설정하여 실행합니다. 또한 앞서 Consul Client를 위해 생성한 인증서를 활용합니다.
$ export CONSUL_HTTP_SSL=true
+$ export CONSUL_HTTP_ADDR=https://127.0.0.1:8501
+$ consul connect envoy -gateway=mesh -register -expose-servers \
+ -service "mesh-gateway-secondary" \
+ -ca-file=/root/consul-cert/consul-agent-ca.pem \
+ -client-cert=/root/consul-cert/vm-dc-client-consul-0.pem \
+ -client-key=/root/consul-cert/vm-dc-client-consul-0-key.pem \
+ -address '{{ GetInterfaceIP "lo" }}:9100' \
+ -wan-address '{{ GetInterfaceIP "eth0" }}:9100' -admin-bind=127.0.0.1:19001 &
+
&
를 붙였습니다. 원하지 않으시면 제거하여 포그라운드로 띄우셔도 됩니다.실행 후에는 Consul UI에서도 해당 Mesh Gateway를 확인할 수 있습니다.
Frontend 애플리케이션을 BM/VM 환경에 구성하고 Backend를 Kubernetes에 구성하는 예제입니다.
https://github.com/hashicorp/demo-consul-101
앞서 2.2.6 테스트를 위한 Pod 생성의 counting 서비스를 활용합니다.
apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: counting
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: counting
+ annotations:
+ 'consul.hashicorp.com/service-tags': servicemesh, consul, counting, v1
+ 'consul.hashicorp.com/connect-inject': 'true'
+spec:
+ containers:
+ - name: counting
+ image: hashicorp/counting-service:0.0.2
+ ports:
+ - containerPort: 9001
+ name: http
+ serviceAccountName: counting
+
BM/VM 환경에서 Frontend 애플리케이션을 구성합니다. Envoy Proxy를 Sidecar로 구성하여 Service Mesh를 위한 구성을 하고, 다른 Consul 데이터센터에 있는 서비스를 찾을 수 있도록 합니다.
https://github.com/hashicorp/demo-consul-101/tree/master/services/dashboard-service
애플리케이션 실행을 위해서는 golang이 설치되어야 합니다.
Dashboard 애플리케이션을 실행합니다.
$ PORT=9003 COUNTING_SERVICE_URL="http://localhost:5000" go run main.go &
+
9003으로 실행된 Dashboard 애플리케이션을 Consul에 서비스로 등록합니다. Consul의 기본 Configuration 디렉토리 위치에 해당 서비스 구성을 작성하고 읽어올 수 있습니다. (e.g. /etc/consul.d)
# /etc/consul.d/dashboard.hcl
+service {
+ name = "dashboard-vm"
+ port = 9003
+
+ connect {
+ sidecar_service {
+ proxy {
+ upstreams = [
+ {
+ destination_name = "counting"
+ datacenter = "k8s-dc"
+ local_bind_port = 5000
+ mesh_gateway {
+ mode = "local"
+ }
+ }
+ ]
+ }
+ }
+ }
+
+ check {
+ id = "dashboard-check"
+ http = "http://localhost:9003/health"
+ method = "GET"
+ interval = "1s"
+ timeout = "1s"
+ }
+}
+
Dashboard 애플리케이션에서 COUNTING_SERVICE_URL
의 대상을 5000번 포트로 지정하였기 때문에 upstream에서 바인딩되는 포트를 맞춰줍니다. 구성이 완료되면 consul reload
명령을 통해 구성 디렉토리의 파일을 반영하고 추가된 서비스를 확인합니다.
$ consul reload
+Configuration reload triggered
+
+$ consul catalog services
+consul
+dashboard-vm
+
다음으로 Dashboard 서비스를 위한 Sidecar를 실행하고 추가된 서비스를 확인합니다.
$ consul connect envoy -sidecar-for dashboard-vm \
+ -ca-file=/root/consul-cert/consul-agent-ca.pem \
+ -client-cert=/root/consul-cert/vm-dc-client-consul-0.pem \
+ -client-key=/root/consul-cert/vm-dc-client-consul-0-key.pem &
+
+$ consul catalog services
+consul
+dashboard-vm
+dashboard-vm-sidecar-proxy
+
이제 구성된 9003번 포트를 통해 Frontend에서 외부 데이터센터의 Backend로 요청이 되는지 확인합니다.
Sidecar기능이 활성화 되면서 Consul의 Intention기능을 사용할 수 있습니다. Intention을 통해 동적으로 서비스에 대한 트래픽을 통제할 수 있습니다.
UI 또는 CLI를 통해 dashboard-vm
이 counting
에 접근할 수 없도록 정의합니다.
$ consul intention create -deny -replace dashboard-vm counting
+Created: dashboard-vm => counting (deny)
+
접근할 수 없게 설정되었기 때문에 Sidecar에 주입된 설정으로 Dashboard에서는 Counting서비스에 접근할 수 없다는 메시지를 출력합니다.
https://www.consul.io/docs/discovery/services
https://learn.hashicorp.com/tutorials/consul/service-registration-health-checks?in=consul/developer-discovery#tuning-scripts-to-be-compatible-with-consul
Consul config 디렉토리 하위에 monitor.hcl파일을 만듭니다.
services {
+ id = "web-service"
+ namd = "web-service"
+ address = "10.10.10.201"
+ port = 8080
+ checks = [
+ {
+ script = "/opt/consul/script/ps-check.sh"
+ interval = "180s"
+ }
+ ]
+}
+
Consul에서 스크립트 기반의 설정시 Config파일 내에 하기와 같은 옵션이 추가되어야 합니다.
(기존설정)
+.....
+enable_script_checks = "true" 또는
+enable_local_script_checks = "true"
+
+
참고 Url : https://www.consul.io/docs/agent/options#_enable_script_checks
consul monitor -log-level=debug
+
==정상적인 경우
2020/10/19 16:21:23 [INFO] raft: Node at 10.90.168.42:8300 [Candidate] entering Candidate state in term 3732
+2020/10/19 16:21:23 [DEBUG] raft: Votes needed: 2
+2020/10/19 16:21:23 [DEBUG] raft: Vote granted from foobar in term 3732. Tally: 1
+
== 비 정상적인 경우
2020/10/19 16:28:53 [WARN] raft: Election timeout reached, restarting election
+2020/10/19 16:28:53 [INFO] raft: Node at 00.00.000.00:8300 [Candidate] entering Candidate state in term 992
+2020/10/19 16:28:53 [DEBUG] raft: Votes needed: 2
+2020/10/19 16:28:53 [DEBUG] raft: Vote granted from foobar2 in term 992. Tally: 1
+2020/10/19 16:28:53 [ERR] raft: Failed to make RequestVote RPC to {Voter <Voter ID>)
+
복구 시 정상화된 로그
+020/10/19 16:29:04 [WARN] raft: Election timeout reached, restarting election
+2020/10/19 16:29:04 [INFO] raft: Node at 00.00.000.00:8300 [Candidate] entering Candidate state in term 989
+2020/10/19 16:29:04 [DEBUG] raft: Votes needed: 2
+2020/10/19 16:29:04 [DEBUG] raft: Vote granted from <ID> in term 989. Tally: 1
+
AmazonLinux 환경에서 하기와 같은 명령어로 consul 설치 후 systemd 를 통한 Consul 시작시 오류 발생
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo
+sudo yum -y install consul
+
[ec2-user@ip-10-0-10-35 ~]$ sudo systemctl start consul
+Job for consul.service failed because a configured resource limit was exceeded. See "systemctl status consul.service" and "journalctl -xe" for details.
+
yum 명령어로 consul설치시 /etc/consul.d/ 경로에 기본적으로 consul.env
파일이 자동으로 생성되는데 해당 파일이 생성되지 않아 수동으로 생성함.
Consul Version : 1.9.x
Helm Chart : 0.30.0
Consul을 쿠버네티스 상에 구성하게 되면 annotation
구성만으로도 쉽게 Sidecar를 애플리케이션과 함께 배포 가능하다.
참고 : Controlling Injection Via Annotation
annotations:
+ 'consul.hashicorp.com/connect-inject': 'true'
+
Consul Sidecar가 Pod 배포시 함께 구성되야 하는 것이 정상이나, Sidecar의 생성 실패나 이미지 가져오기 실패라는 언급도 없이 Sidecar의 injection이 동작하지 않는 경우가 있다.
쿠버네티스 상의 Consul을 구성하게 되면 injector가 Sidecar를 함께 배포하는 작업을 수행하므로 먼저 해당 컴포넌트의 로그를 확인한다.
kubectl logs -n consul -l component=connect-injector -f
+
annotation
의 동작은 쿠버네티스 컨트롤 플래인, 즉, 쿠버네티스의 API를 통해 요청되므로 해당 API를 통해 Consul에 접근이 가능한지 확인이 필요하다.
consul-inject에서 kubernetest api 접속이 불가하다면 500
에러가 발생한다.
$ kubectl proxy
+Starting to serve on 127.0.0.1:8001
+
$ curl -vv localhost:8001/api/v1/namespaces/consul/services/https:consul-connect-injector-svc:443/proxy/health/ready
+* Trying 127.0.0.1...
+* TCP_NODELAY set
+* Connected to localhost (127.0.0.1) port 8001 (#0)
+> GET /api/v1/namespaces/consul/services/https:consul-connect-injector-svc:443/proxy/health/ready HTTP/1.1
+> Host: localhost:8001
+> User-Agent: curl/7.61.1
+> Accept: */*
+>
+< HTTP/1.1 204 No Content
+< Audit-Id: 52947d1d-0c90-47eb-8dc2-6c2efa0193fa
+< Cache-Control: no-cache, private
+< Date: Fri, 06 Aug 2021 10:15:21 GMT
+<
+* Connection #0 to host localhost left intact
+
* Trying 127.0.0.1...
+* TCP_NODELAY set
+* Connected to localhost (127.0.0.1) port 8001 (#0)
+> GET /api/v1/namespaces/consul/services/https:consul-connect
+-injector-svc:443/proxy/health/ready HTTP/1.1
+> Host: localhost:8001
+> User-Agent: curl/7.61.1
+> Accept: */*
+>
+< HTTP/1.1 500 Internal Server Error
+< Audit-Id: acb30d91-d8db-463e-a91e-1e2a5382329e
+< Cache-Control: no-cache, private
+< Content-Length: 178
+< Content-Type: application/json
+< Date: Fri, 06 Aug 2021 11:04:38 GMT
+<
+{
+ "kind": "Status",
+ "apiVersion": "v1",
+ "metadata": {
+
+ },
+ "status": "Failure",
+ "message": "error trying to reach service: Address is not a
+ llowed",
+ "code": 500
+}
+* Connection #0 to host localhost left intact
+
[debug] [router] upstream reset: reset reason: connection termination, transport failure reason.
+[debug] [http] Sending local reply with details upstream_reset_before_response_started(connection termination).
+
원인 1: Envoy에서 TCP 연결(FIN)이 닫는 현상 보고됨 - Keepalive time 이슈
해결 1 - 1: Keepalive time을 끄거나
해결 1 - 2: max_requests_per_connection 을 1로 설정
해결 1 - 3: Keepalive interval을 짧게 (10초)
원인 2 : Kubernetes 내부 기본 TCP 계층 4 연결 부하 분산의 제한, (e.g. tomcat - maxKeepAliveRequests)
해결 2 - 1: 애플리케이션의 KeepAlive를 끔
원인 3 : 외부 서비스에 대해 요청시 Envoy Proxy는 애플리케이션이 연결을 닫으려 하지 않는 한 영구적으로 연결을 닫지 않으므로 신규요청 발생시 IPSET이 만료되는 (1시간) 시간이 지나 DNS 확인 없이 동일한 IP로 요청하는 경우
해결 3 - 1: Keepalive time을 끄거나
해결 3 - 2: 시간 초과 값을 늘임
해결 3 - 3: dns_refresh_rate 간격을 짧게 (300초)
참고 : https://learn.hashicorp.com/tutorials/consul/consul-template
# apache_install.sh.ctmpl
+#!/bin/bash
+sudo apt-get remove -y apache2
+sudo apt-get install -y apache2={{ key "/apache/version" }}
+
consul kv put apache/version 2.2.14-5ubuntu8.7
$ consul-template -template="./apache_install.sh.ctmpl:./apache_install.sh" -once
+
파일 구조
.
+├── apache_install.sh.ctmpl
+└── `apache_install.sh`
+
#!/bin/bash
+sudo apt-get remove -y apache2
+sudo apt-get install -y apache2=2.2.14-5ubuntu8.7
+
# consul-template-apache-install.hcl
+consul {
+ address = "localhost:8500"
+
+ retry {
+ enabled = true
+ attempts = 12
+ backoff = "250ms"
+ }
+}
+template {
+ source = "./apache_install.sh.ctmpl"
+ destination = "./apache_install.sh"
+ perms = 0644
+ command = "echo './apache_install.sh'"
+}
+
$ consul-template -config=consul-template-apache-install.hcl
+apache_install.sh
+
참고 : https://learn.hashicorp.com/tutorials/consul/load-balancing-nginx
# nginx.conf.ctmpl
+upstream backend {
+ {{- range service "nginx-backend" }}
+ server {{.Address}}:{{.Port}} max_fails=3 fail_timeout=60 weight=1;
+ {{else}}server 127.0.0.1:65535; # force a 502
+ {{- end}}
+}
+
+server {
+ listen 80 default_server;
+
+ location /stub_status {
+ stub_status;
+ }
+
+ location / {
+ proxy_pass http://backend;
+ }
+}
+
$ consul-template -template="./nginx.conf.ctmpl:./nginx.conf"
+
파일 구조
.
+├── nginx.conf.ctmpl
+└── `nginx.conf`
+
# consul-template-nginx.hcl
+consul {
+ address = "localhost:8500"
+
+ retry {
+ enabled = true
+ attempts = 12
+ backoff = "250ms"
+ }
+}
+template {
+ source = "./nginx.conf.ctmpl"
+ destination = "./nginx.conf"
+ perms = 0644
+ command = "echo 'service nginx reload'"
+}
+
$ consul-template -config=consul-template-nginx.hcl
+service nginx reload
+
팁
실습을 위한 조건은 다음과 같습니다.
참고 : https://learn.hashicorp.com/collections/consul/kubernetes-production
다운받은 Consul 바이너리를 통해 Gossip 암호화 키를 생성합니다
kubectl create secret generic consul-gossip-encryption-key --from-literal=key=$(consul keygen)
+
발급받은 라이선스 파일을 저장(e.g. consul.hclic)하고 Kubernetes의 secret으로 적용합니다.
kubectl create secret generic license --from-file='key=./consul.hclic'
+
Helm repo add & update
helm repo add hashicorp https://helm.releases.hashicorp.com && \
+helm repo update
+
github : https://github.com/hashicorp/consul-helm/blob/master/values.yaml
Helm 설치를 위한 파일(e.g. value.yaml) 을 작성합니다.
enterpriseLicense
항목의 주석을 해제합니다.global:
+ enabled: true
+ name: consul
+ image: hashicorp/consul-enterprise:1.11.3-ent
+ enableConsulNamespaces: true
+ adminPartitions:
+ enabled: false
+ datacenter: dc1
+ # enterpriseLicense:
+ # secretName: license
+ # secretKey: key
+ gossipEncryption:
+ secretName: consul-gossip-encryption-key
+ secretKey: key
+ tls:
+ enabled: false
+ enableAutoEncrypt: true
+ enableConsulNamespaces: true
+
+client:
+ enabled: true
+ grpc: true
+
+connectInject:
+ enabled: true
+ replicas: 2
+
+dns:
+ enabled: true
+
+controller:
+ enabled: true
+
+syncCatalog:
+ enabled: true
+ toConsul: false
+ consulNamespaces:
+ mirroringK8S: true
+
+
kubectl config use-context $(grep gs-cluster-0 KCONFIG.txt)
+helm install consul -f ./values.yaml hashicorp/consul --version v0.40.0 --debug
+
kubectl port-forward service/consul-server 8500:8500
+
팁
실습을 위한 조건은 다음과 같습니다.
connectInject
이 활성화 되어있어야 합니다.Consul 서비스 메시를 사용하면 애플리케이션을 제로 트러스트 네트워크에 배포할 수 있습니다. 제로 트러스트 네트워크는 아무 것도 자동으로 신뢰되지 않는 네트워크입니다. 모든 연결은 인증과 승인을 모두 받아야 합니다. 이 패러다임은 동일한 네트워크에서 다수의 서비스가 실행될 수 있는 마이크로서비스 및 멀티 클라우드 환경에서 중요합니다. Consul 서비스 메시를 사용하면 mTLS를 사용하여 서비스 ID를 인증하고 의도를 사용하여 서비스 작업을 승인하거나 차단할 수 있습니다.
이 튜토리얼에서는 두 개의 서비스 web
및 api
를 Kubernetes 클러스터에서 실행되는 Consul의 서비스 메시에 배포합니다. 두 서비스는 Consul을 사용하여 서로를 검색하고 사이드카 프록시를 사용하여 mTLS를 통해 통신합니다. 두 서비스는 웹UI와 백엔드 서비스로 구성된 간단한 2-tier 애플리케이션으로 HTTP를 통해 서비스와 통신합니다.
Sidecar Proxy는 애플리케이션 컨테이너와 함께 동일 Pod상에 배포됨으로 localhost 로 통신할 수 있습니다. 사용자가 다른서비스에 대한 요청을 Sidecar Proxy에 지정하면 해당 요청을 맵핑된 다른 서비스로 전달합니다. 이 방식은 기존 개발자가 로컬에서 개발하는 환경과 동일하게 동작합니다. UI웹을 로컬 9090포트로 실행하고 백엔드 앱을 8080 포트로 실행한경우 UI웹은 백엔드 앱을 localhost:8080
으로 호출합니다. Consul Sidecar Proxy는 localhost 로 요청되는 포트를 지정한 Upstream
서비스로 전달하는 규칙을 처리하며, 여기에는 mTLS가 자동으로 구성됩니다.
Sidecar Proxy의 서비스 접근 제어를 위해 모든 서비스에 대한 Deny 구성을 수행합니다. UI의 좌측 메뉴의 Intention
을 클릭하고 우측의 Create
버튼을 클릭하여 모든 서비스 (엔터프라이즈의 경우 모든 Namespace 포함) 간에 Deny 규칙을 생성합니다.
테스트 구성을 저장하기 위한 디렉토리를 생성합니다.
mkdir ./k8s_config
+
cat > ./k8s_config/api.yaml <<EOF
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: api-v1
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: api-v1
+spec:
+ selector:
+ app: api-v1
+ ports:
+ - port: 9091
+ targetPort: 9091
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: api-v1
+ labels:
+ app: api-v1
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: api-v1
+ template:
+ metadata:
+ labels:
+ app: api-v1
+ annotations:
+ consul.hashicorp.com/connect-inject: 'true'
+ spec:
+ serviceAccountName: api-v1
+ containers:
+ - name: api
+ image: nicholasjackson/fake-service:v0.7.8
+ ports:
+ - containerPort: 9091
+ env:
+ - name: 'LISTEN_ADDR'
+ value: '127.0.0.1:9091'
+ - name: 'NAME'
+ value: 'api-v1'
+ - name: 'MESSAGE'
+ value: 'Response from API v1'
+EOF
+
cat > ./k8s_config/web.yaml <<EOF
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: web
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: web
+spec:
+ selector:
+ app: web
+ ports:
+ - port: 9090
+ targetPort: 9090
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: web-deployment
+ labels:
+ app: web
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: web
+ template:
+ metadata:
+ labels:
+ app: web
+ annotations:
+ consul.hashicorp.com/connect-inject: 'true'
+ consul.hashicorp.com/connect-service-upstreams: 'api-v1:9091'
+ spec:
+ serviceAccountName: web
+ containers:
+ - name: web
+ image: nicholasjackson/fake-service:v0.7.8
+ ports:
+ - containerPort: 9090
+ env:
+ - name: 'LISTEN_ADDR'
+ value: '0.0.0.0:9090'
+ - name: 'UPSTREAM_URIS'
+ value: 'http://localhost:9091'
+ - name: 'NAME'
+ value: 'web'
+ - name: 'MESSAGE'
+ value: 'Hello World'
+EOF
+
프론트엔드 서비스에 Deployment
구성 내용의 annotation
을 확인하세요. 이 형식은 9091로 요청된 localhost로의 요청을 sidecar가 api-v1
서비스로 전달하는 것을 의미합니다.
consul.hashicorp.com/connect-service-upstreams: 'api-v1:9091'
kubectl apply
명령을 통해 배포를 확인하고 Consul UI에서 확인합니다.
kubectl apply -f ./k8s_config/api.yaml
+
# 출력
+serviceaccount/api-v1 created
+service/api-v1 created
+deployment.apps/api-v1 created
+
kubectl apply -f ./k8s_config/web.yaml
+
# 출력
+serviceaccount/web created
+service/web created
+deployment.apps/web-deployment created
+
UI에 접속하고 좌측 Namespace에서 사용중인 Namespace를 확인합니다. 서비스 목록에 api-v1
, web
이 등록되고 상태가 정상임을 확인합니다.
port-forward
를 통해 로컬에서 web 앱을 확인합니다.
kubectl port-forward service/web 9090:9090 --address 0.0.0.0
+
# 출력
+Forwarding from 0.0.0.0:9090 -> 9090
+
http://localhost:9090/ui 에 브라우저로 접속하여 상태를 확인합니다.
500 에러가 발생하였습니다. Consul Service Mesh는 서비스간 의도적으로 접속 가능여부를 동적으로 통제합니다. 이 기능을 Intention
이라고 부릅니다. Consul UI에 접속하여 web
서비스 이름을 클릭하면 다음과 같이 요청이 거부되어 있는것을 확인할 수 있습니다.
Intention 수정을 위해서는 권한이 필요합니다. 현재 환경에서는 전달받은 token (3108cbb3-005c-a3e4-9a42-6f13d1f5e4e6) 을 우측 상단 로그인에서 입력합니다.
x
표시를 클릭하여 Create
버튼을 클릭하여 Intention 규칙을 생성합니다. 이후에 연결이 허용된것을 확인할 수 있습니다.
다시 http://localhost:9090/ui 에 브라우저로 접속하여 상태를 확인합니다.
다음 과정을 진행하기 위해 기존 적용된 구성을 삭제합니다.
kubectl delete -f ./k8s_config
+
# 출력
+serviceaccount "api-v1" deleted
+service "api-v1" deleted
+deployment.apps "api-v1" deleted
+serviceaccount "web" deleted
+service "web" deleted
+deployment.apps "web-deployment" deleted
+
백엔드 서비스를 다른 Namespace에 배포하고, 프론트엔드가 해당 백엔드에 접근할 수 있도록 수정합니다.
cat > ./k8s_config/web.yaml <<EOF
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: web
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: web
+spec:
+ selector:
+ app: web
+ ports:
+ - port: 9090
+ targetPort: 9090
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: web-deployment
+ labels:
+ app: web
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: web
+ template:
+ metadata:
+ labels:
+ app: web
+ annotations:
+ consul.hashicorp.com/connect-inject: 'true'
+ consul.hashicorp.com/connect-service-upstreams: 'api-v1.<namespace>:9091'
+ spec:
+ serviceAccountName: web
+ containers:
+ - name: web
+ image: nicholasjackson/fake-service:v0.7.8
+ ports:
+ - containerPort: 9090
+ env:
+ - name: 'LISTEN_ADDR'
+ value: '0.0.0.0:9090'
+ - name: 'UPSTREAM_URIS'
+ value: 'http://localhost:9091'
+ - name: 'NAME'
+ value: 'web'
+ - name: 'MESSAGE'
+ value: 'Hello World'
+EOF
+
프론트엔드 서비스에 Deployment
구성 내용의 annotation
을 확인하세요. 이 형식은 9091로 요청된 localhost로의 요청을 sidecar가 <namespace>
Namespace의 api-v1
서비스로 전달하는 것을 의미합니다.
consul.hashicorp.com/connect-service-upstreams: 'api-v1.<namespace>:9091'
또한 Namespace 간 Intention을 작성하여 적용해야합니다.
참고 - Intention 순위
범위가 좁을수록 우선순위가 높습니다.
Source Namespace | Source Name | Destination Namespace | Destination Name | 높을수록 서열 높음 |
---|---|---|---|---|
Exact | Exact | Exact | Exact | 9 |
Exact | * | Exact | Exact | 8 |
* | * | Exact | Exact | 7 |
Exact | Exact | Exact | * | 6 |
Exact | * | Exact | * | 5 |
* | * | Exact | * | 4 |
Exact | Exact | * | * | 3 |
Exact | * | * | * | 2 |
* | * | * | * | 1 |
Consul 1.9 이전에는 Kubernetes에서 Consul과 함께 구성 항목을 사용할 때 운영자가 실행 중인 컨테이너에 들어가거나 로컬 Consul 바이너리를 사용하여 구성해야 했습니다. 1.9 이전 버전에서는 구성 항목을 Consul CLI, HTTP API로 관리하거나 시작하는 동안 구성 파일로 에이전트에 제공해야 합니다.
Consul 1.9부터 대부분의 구성 항목은 Kubernetes 사용자 지정 리소스 정의(CRD)로 관리할 수 있습니다. 이제 대부분의 구성 항목을 YAML로 정의하고 익숙한 kubectl apply
명령을 사용하여 Consul에 등록할 수 있습니다.
현재 Kubernetes에서 Consul의 CRD로 사용할 수 있는 구성 항목은 다음과 같습니다.
proxy-defaults
- 프록시 구성 제어service-defaults
- 주어진 서비스의 모든 인스턴스에 대한 기본값을 구성합니다.service-resolver
- 서비스 인스턴스를 특정 Connect 업스트림 검색 요청과 일치시킵니다.service-router
- HTTP 경로를 기반으로 레이어 7 트래픽을 보낼 위치를 정의합니다.service-splitter
- 백분율에 따라 단일 HTTP 경로에 대한 요청을 나누는 방법을 정의합니다.service-intentions
- 특정 서비스 대 서비스 상호 작용에 대한 제한을 정의합니다.필요한 애플리케이션을 다운로드 받습니다. git clone 또는 https://github.com/hashicorp/learn-consul-kubernetes 로 접속하여 Code를 다운로드 받습니다.
git clone
git clone https://github.com/hashicorp/learn-consul-kubernetes.git
+
Code download
다운로드 후 learn-consul-kubernetes/service-mesh/deploy
경로로 이동하고 샘플 구성을 반영합니다.
cd learn-consul-kubernetes/service-mesh/deploy
+kubectl apply -f hashicups/
+
# 출력
+service/frontend created
+serviceaccount/frontend created
+servicedefaults.consul.hashicorp.com/frontend created
+configmap/nginx-configmap created
+deployment.apps/frontend created
+service/postgres created
+serviceaccount/postgres created
+servicedefaults.consul.hashicorp.com/postgres created
+deployment.apps/postgres created
+service/product-api created
+serviceaccount/product-api created
+servicedefaults.consul.hashicorp.com/product-api created
+configmap/db-configmap created
+deployment.apps/product-api created
+service/public-api created
+serviceaccount/public-api created
+servicedefaults.consul.hashicorp.com/public-api created
+deployment.apps/public-api created
+
서비스는 Consul이 각 서비스에 대한 프록시를 자동으로 삽입할 수 있도록 하는 annotation
을 사용합니다. 프록시 는 Consul의 구성을 기반으로 서비스 간의 요청을 처리하기 위해 데이터 플레인을 생성합니다. Consul이 주입되는 label을 선택하여 프록시가 있는 응용 프로그램을 확인할 수 있습니다.
kubectl get pods --selector consul.hashicorp.com/connect-inject-status=injected
+
# 출력
+NAME READY STATUS RESTARTS AGE
+frontend-98cb6859b-6ndvk 2/2 Running 0 3m10s
+postgres-6ccb6d9968-hkbgz 2/2 Running 0 3m9s
+product-api-6798bc4b4d-9ddv4 2/2 Running 2 3m9s
+public-api-5bdf986897-tlxj2 2/2 Running 0 3m9s
+
배포된 앱에 접근하기 위해 port-forward
를 구성합니다.
kubectl port-forward service/frontend 18080:80 --address 0.0.0.0
+
# 출력
+Forwarding from 0.0.0.0:18080 -> 80
+
브라우저에서 http://localhost:18080 로 접근합니다.
현재 Intention
규칙이 모두 deny
로 구성되어있다면 에러 화면을 확인하게 됩니다.
UI에서가 아닌 CRD를 통해 Intention
을 정의하기위해 아래와 같이 구성합니다.
cat > ./service-to-service.yaml <<EOF
+apiVersion: consul.hashicorp.com/v1alpha1
+kind: ServiceIntentions
+metadata:
+ name: frontend-to-public-api
+spec:
+ destination:
+ name: public-api
+ sources:
+ - name: frontend
+ action: allow
+---
+apiVersion: consul.hashicorp.com/v1alpha1
+kind: ServiceIntentions
+metadata:
+ name: public-api-to-product-api
+spec:
+ destination:
+ name: product-api
+ sources:
+ - name: public-api
+ action: allow
+---
+apiVersion: consul.hashicorp.com/v1alpha1
+kind: ServiceIntentions
+metadata:
+ name: product-api-to-postgres
+spec:
+ destination:
+ name: postgres
+ sources:
+ - name: product-api
+ action: allow
+EOF
+
규칙의 내용은 다음과 같습니다.
규칙을 적용합니다.
kubectl apply -f service-to-service.yaml
+
# 출력
+serviceintentions.consul.hashicorp.com/frontend-to-public-api created
+serviceintentions.consul.hashicorp.com/public-api-to-product-api created
+serviceintentions.consul.hashicorp.com/product-api-to-postgres created
+
Consul UI에서 확인해보면 해당 Intention 규칙은 CRD로 적용되었기 때문에 Managed by CRD
표시가 붙는것을 확인할 수 있습니다.
배포된 앱에 접근하기 위해 port-forward
를 구성합니다.
kubectl port-forward service/frontend 18080:80 --address 0.0.0.0
+
# 출력
+Forwarding from 0.0.0.0:18080 -> 80
+
브라우저에서 http://localhost:18080 로 접근합니다.
서비스 간 연결이 허용되었으므로 페이지가 잘 표시됩니다.
다음 과정을 위해 배포된 리소스를 정리합니다.
kubectl delete -f service-to-service.yaml
+
# 출력
+serviceintentions.consul.hashicorp.com "frontend-to-public-api" deleted
+serviceintentions.consul.hashicorp.com "public-api-to-product-api" deleted
+serviceintentions.consul.hashicorp.com "product-api-to-postgres" deleted
+
kubectl delete -f hashicups/
+
# 출력
+service "frontend" deleted
+serviceaccount "frontend" deleted
+servicedefaults.consul.hashicorp.com "frontend" deleted
+configmap "nginx-configmap" deleted
+deployment.apps "frontend" deleted
+service "postgres" deleted
+serviceaccount "postgres" deleted
+servicedefaults.consul.hashicorp.com "postgres" deleted
+deployment.apps "postgres" deleted
+service "product-api" deleted
+serviceaccount "product-api" deleted
+servicedefaults.consul.hashicorp.com "product-api" deleted
+configmap "db-configmap" deleted
+deployment.apps "product-api" deleted
+service "public-api" deleted
+serviceaccount "public-api" deleted
+servicedefaults.consul.hashicorp.com "public-api" deleted
+deployment.apps "public-api" deleted
+
실습을 진행하기 위한 디렉토리를 생성합니다.
mkdir ./traffic
+
Service Mesh는 HTTP 프로토콜 상에서 L7으로 동작하게 됩니다. 따라서 기본 프로토콜을 http로 변경합니다.
cat > ./traffic/service-to-service.yaml <<EOF
+apiVersion: consul.hashicorp.com/v1alpha1
+kind: ProxyDefaults
+metadata:
+ name: global
+spec:
+ config:
+ protocol: http
+EOF
+
kubectl apply -f ./traffic/service-to-service.yaml
+
# 출력
+proxydefaults.consul.hashicorp.com/global created
+
cat > ./traffic/gs-frontend.yaml <<EOF
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: gs-frontend
+spec:
+ selector:
+ app: gs-frontend
+ ports:
+ - protocol: TCP
+ port: 3000
+ targetPort: 3000
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: gs-frontend
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: gs-frontend
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: gs-frontend
+ template:
+ metadata:
+ labels:
+ app: gs-frontend
+ annotations:
+ prometheus.io/scrape: "true"
+ prometheus.io/port: "9901"
+ consul.hashicorp.com/connect-inject: "true"
+ consul.hashicorp.com/transparent-proxy: true
+ consul.hashicorp.com/connect-service-upstreams: "gs-backend:8080"
+ spec:
+ serviceAccountName: gs-frontend
+ containers:
+ - name: gs-frontend
+ image: hahohh/consul-frontend-nodejs:v1.5
+ env:
+ - name: PORT
+ value: "3000"
+ - name: UPSTREAM_URL
+ value: "http://localhost:8080
+ ports:
+ - containerPort: 3000
+EOF
+
적용하기
kubectl apply -f ./traffic/gs-frontend.yaml
+
cat > ./traffic/gs-backend.yaml <<EOF
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: gs-backend
+spec:
+ selector:
+ app: gs-backend
+ ports:
+ - protocol: TCP
+ port: 8080
+ targetPort: 8080
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: gs-backend
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: gs-backend-v1
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: gs-backend
+ version: v1
+ template:
+ metadata:
+ labels:
+ app: gs-backend
+ version: v1
+ annotations:
+ consul.hashicorp.com/connect-inject: "true"
+ consul.hashicorp.com/service-meta-version: v1
+ consul.hashicorp.com/service-tags: v1
+ spec:
+ serviceAccountName: gs-backend
+ containers:
+ - name: gs-backend
+ image: hahohh/consul-backend-go:v1.2
+ env:
+ - name: PORT
+ value: "8080"
+ - name: COLOR
+ value: "green"
+ - name: VERSION
+ value: "v1"
+ ports:
+ - containerPort: 8080
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: gs-backend-v2
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: gs-backend
+ version: v2
+ template:
+ metadata:
+ labels:
+ app: gs-backend
+ version: v2
+ annotations:
+ consul.hashicorp.com/connect-inject: "true"
+ consul.hashicorp.com/service-meta-version: v2
+ consul.hashicorp.com/service-tags: v2
+ spec:
+ serviceAccountName: gs-backend
+ containers:
+ - name: gs-backend
+ image: hahohh/consul-backend-go:v1.2
+ env:
+ - name: PORT
+ value: "8080"
+ - name: COLOR
+ value: "blue"
+ - name: VERSION
+ value: "v2"
+ # - name: ISFAIL
+ # value: "yyyy"
+ ports:
+ - containerPort: 8080
+EOF
+
적용하기
kubectl apply -f ./traffic/gs-backend.yaml
+
cat > ./traffic/service-to-service.yaml <<EOF
+apiVersion: consul.hashicorp.com/v1alpha1
+kind: ServiceIntentions
+metadata:
+ name: gs-backend
+spec:
+ destination:
+ name: gs-backend
+ sources:
+ - name: gs-frontend
+ action: allow
+EOF
+
적용하기
kubectl apply -f ./traffic/service-to-service.yaml
+
port-forward
를 통해 로컬에서 web 앱을 확인합니다.
kubect l port-forward service/gs-frontend 3000:3000 --address 0.0.0.0
+
# 출력
+Forwarding from 0.0.0.0:3000 -> 3000
+
http://localhost:3000 에 브라우저로 접속하여 상태를 확인합니다.
두개의 버전의 백엔드가 서로다른 갑을 리턴하여 때에 따라 v1, v2가 번갈아 나타납니다.
cat > ./traffic/service-resolver.yaml <<EOF
+apiVersion: consul.hashicorp.com/v1alpha1
+kind: ServiceResolver
+metadata:
+ name: gs-backend
+spec:
+ defaultSubset: v1
+ subsets:
+ v1:
+ filter: "Service.Meta.version == v1"
+ v2:
+ filter: "Service.Meta.version == v2"
+EOF
+
적용하기
kubectl apply -f ./traffic/service-resolver.yaml
+
앞서 배포된 gs-backend
의 Deployment
에 선언된 annotation
내용을 확인하면 consul.hashicorp.com/service-meta-version: v2
을 확인할 수 있습니다. Consul UI에서도 해당 Meta 정보를 확인할 수 있습니다. 선언된 정보를 기반으로 서비스의 subset
을 정의합니다.
cat > ./traffic/service-splitter.yaml <<EOF
+apiVersion: consul.hashicorp.com/v1alpha1
+kind: ServiceSplitter
+metadata:
+ name: gs-backend
+spec:
+ splits:
+ - weight: 50
+ serviceSubset: v1
+ - weight: 50
+ serviceSubset: v2
+EOF
+
적용하기
kubectl apply -f ./traffic/service-splitter.yaml
+
weight
에 지정된 비율로 Resolve된 서비스 대상 subset
에 트래픽을 분산합니다. weight
값을 0:100, 100:0 등으로 변경하여 요청의 결과가 어떻게 변화하는지 확인해 봅니다.
cat > ./traffic/service-router.yaml <<EOF
+apiVersion: consul.hashicorp.com/v1alpha1
+kind: ServiceRouter
+metadata:
+ name: gs-backend
+spec:
+ routes:
+ - match:
+ http:
+ pathPrefix: '/v1'
+ destination:
+ service: gs-backend
+ serviceSubset: v1
+ - match:
+ http:
+ pathPrefix: '/v2'
+ destination:
+ service: gs-backend
+ serviceSubset: v2
+ - match:
+ http:
+ queryParam:
+ - name: version
+ exact: 'v1'
+ destination:
+ service: gs-backend
+ serviceSubset: v1
+ - match:
+ http:
+ queryParam:
+ - name: version
+ exact: 'v2'
+ destination:
+ service: gs-backend
+ serviceSubset: v2
+EOF
+
적용하기
kubectl apply -f ./traffic/service-router.yaml
+
예제에서는 url의 path, queryParam을 예로 트래픽을 컨트롤 합니다. 다음과같이 요청하여 트래픽이 조정되는 것을 확인합니다.
service-to-service 허용 방식에도 Meshod, Path 등을 지정할 수 있습니다. 다음과 같이 변경하고 POST만 넣은 상태에서는 어떻게 동작하는지 확인합니다.
cat > ./traffic/service-to-service.yaml <<EOF
+apiVersion: consul.hashicorp.com/v1alpha1
+kind: ServiceIntentions
+metadata:
+ name: gs-backend
+spec:
+ destination:
+ name: gs-backend
+ sources:
+ - name: gs-frontend
+ permissions:
+ - action: allow
+ http:
+ pathPrefix: /
+ # methods: ['GET', 'PUT', 'POST', 'DELETE', 'HEAD']
+ methods: ['POST']
+
적용하기
kubectl apply -f ./traffic/service-to-service.yaml
+
다시 앱간의 요청인 GET
으로 변경하고 트래픽 허용여부를 확인해봅니다.
cat > ./traffic/service-to-service.yaml <<EOF
+apiVersion: consul.hashicorp.com/v1alpha1
+kind: ServiceIntentions
+metadata:
+ name: gs-backend
+spec:
+ destination:
+ name: gs-backend
+ sources:
+ - name: gs-frontend
+ permissions:
+ - action: allow
+ http:
+ pathPrefix: /
+ # methods: ['GET', 'PUT', 'POST', 'DELETE', 'HEAD']
+ methods: ['GET']
+
적용하기
kubectl apply -f ./traffic/service-to-service.yaml
+
Consul UI에 접속하여 gs-backend
의 Routing
탭을 클릭, 구성된 Resolver, Splitter, Router가 어떻게 구성되었는지, 각 서비스에는 어떤 조건으로 요청할 수 있는지 확인합니다.
Ingress gateway가 8080을 Listen하도록 구성되어있으면, 아래와 같이 해당 포트의 요청을 받을 대상 서비스를 지정합니다.
apiVersion: consul.hashicorp.com/v1alpha1
+kind: IngressGateway
+metadata:
+ name: ingress-gateway
+spec:
+ listeners:
+ - port: 8080
+ protocol: http
+ services:
+ - name: hashicups
+ hosts: ["*"]
+
여기서 지정된 hashicups
는 가상의 서비스 입니다. 해당 서비스에 대한 Service Router 설정을 다음과 같이 구성합니다.
apiVersion: consul.hashicorp.com/v1alpha1
+kind: ServiceRouter
+metadata:
+ name: hashicups
+spec:
+ routes:
+ - match:
+ http:
+ pathPrefix: '/api'
+ destination:
+ service: public-api
+ - match:
+ http:
+ pathPrefix: '/'
+ destination:
+ service: frontend
+
Consul Doc : https://www.consul.io/docs/k8s/connect#kubernetes-pods-with-multiple-ports
annotation
에 다음과 같이 서비스 이름과 대상 포트를 리스트로 지정합니다.
consul.hashicorp.com/connect-inject: true
+consul.hashicorp.com/transparent-proxy: false
+consul.hashicorp.com/connect-service: web,web-admin
+consul.hashicorp.com/connect-service-port: 8080,9090
+
포트가 서비스 이름과 동일한 순서로 나열되는 순서에 입니다. 즉, 첫 번째 서비스 이름 web은 첫 번째 포트인 8080에 해당합니다. 두 번째 서비스 이름 web-admin은 두 번째 포트인 9090에 해당합니다.
이 서비스를 호출하는 타 서비스의 Upstream은 다음과 같을 수 있습니다.
consul.hashicorp.com/connect-service-upstreams: "web:1234,web-admin:2234"
+
Consul API : https://www.consul.io/api-docs/config
Proxy Default : https://www.consul.io/docs/connect/config-entries/proxy-defaults
Envoy Integration : https://www.consul.io/docs/connect/proxies/envoy
cat > ./gs-frontend.yaml <<EOF
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: gs-frontend
+spec:
+ selector:
+ app: gs-frontend
+ ports:
+ - protocol: TCP
+ port: 3000
+ targetPort: 3000
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: gs-frontend
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: gs-frontend
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: gs-frontend
+ template:
+ metadata:
+ annotations:
+ consul.hashicorp.com/connect-inject: "true"
+ consul.hashicorp.com/transparent-proxy: "false"
+ consul.hashicorp.com/connect-service-upstreams: "gs-backend:8080"
+ labels:
+ app: gs-frontend
+ spec:
+ serviceAccountName: gs-frontend
+ containers:
+ - name: gs-frontend
+ image: hahohh/consul-frontend-nodejs:v1.5
+ env:
+ - name: PORT
+ value: "3000"
+ - name: UPSTREAM_URL
+ value: "http://localhost:8080"
+ ports:
+ - containerPort: 3000
+EOF
+
+kubectl apply -f ./gs-frontend.yaml
+
kubectl exec pod/<POD_ID> -c envoy-sidecar -- wget -qO- http://localhost:19000/config_dump
+
{
+ "configs": [
+ {
+ "@type": "type.googleapis.com/envoy.admin.v3.BootstrapConfigDump",
+ "bootstrap": {
+ <생략>
+ "static_resources": {
+ "clusters": [
+ {
+ "name": "local_agent",
+ "type": "STATIC",
+ "connect_timeout": "1s",
+ <생략>
+ {
+ "@type": "type.googleapis.com/envoy.admin.v3.ClustersConfigDump",
+ "static_clusters": [
+ {
+ "cluster": {
+ "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
+ "name": "local_agent",
+ "type": "STATIC",
+ "connect_timeout": "1s",
+ "http2_protocol_options": {},
+
+ <생략>
+ "dynamic_active_clusters": [
+ {
+ "version_info": "eb3fa9f7104047dd6420d0eb13fd556995d2c6e7d687c4db07b759408ecf0345",
+ "cluster": {
+ "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
+ "name": "gs-backend.default.dc1.internal.a3568e3d-e611-5f3e-01b9-24ec787ce275.consul",
+ "type": "EDS",
+ "eds_cluster_config": {
+ "eds_config": {
+ "ads": {},
+ "resource_api_version": "V3"
+ }
+ },
+ "connect_timeout": "5s",
+ "circuit_breakers": {},
+ <생략>
+
cat > ./proxydefaults.yaml <<EOF
+apiVersion: consul.hashicorp.com/v1alpha1
+kind: ProxyDefaults
+metadata:
+ name: global
+spec:
+ config:
+ protocol: http
+ local_connect_timeout_ms: 60000
+ local_request_timeout_ms: 60000
+ upstreamConfig:
+ defaults:
+ protocol: http
+ connectTimeoutMs : 60000
+EOF
+
+kubectl apply -f ./proxydefaults.yaml
+
proxy-default
값 변경 확인kubectl exec pod/consul-server-0 -- wget -qO- http://localhost:8500/v1/config/proxy-defaults/global | jq .
+{
+ "Kind": "proxy-defaults",
+ "Name": "global",
+ "Config": {
+ "local_connect_timeout_ms": 60000,
+ "local_request_timeout_ms": 60000,
+ "protocol": "http",
+ "upstreamConfig": {
+ "defaults": {
+ "connectTimeoutMs": 60000,
+ "protocol": "http"
+ }
+ }
+ },
+ "TransparentProxy": {},
+ "MeshGateway": {},
+ "Expose": {},
+ "Meta": {
+ "consul.hashicorp.com/source-datacenter": "dc1",
+ "external-source": "kubernetes"
+ },
+ "Partition": "default",
+ "Namespace": "default",
+ "CreateIndex": 354,
+ "ModifyIndex": 354
+}
+
{
+ "configs": [
+ {
+ "@type": "type.googleapis.com/envoy.admin.v3.BootstrapConfigDump",
+ "bootstrap": {
+ <생략>
+ {
+ "version_info": "c80b97864daee80ecc0335fdd5b67437b946ea76e77f848d0cd69d6d1ad330ea",
+ "cluster": {
+ "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
+ "name": "local_app",
+ "type": "STATIC",
+ "connect_timeout": "60s",
+ "load_assignment": {
+ <생략>
+ {
+ "name": "public_listener:10.0.1.162:20000",
+ "active_state": {
+ "version_info": "4f8654a4ce70f346dd63c479b3b42b41c5a92eda52f6b2d38dab8ff4536923f3",
+ "listener": {
+ "@type": "type.googleapis.com/envoy.config.listener.v3.Listener",
+ "name": "public_listener:10.0.1.162:20000",
+ "address": {
+ "socket_address": {
+ "address": "10.0.1.162",
+ "port_value": 20000
+ }
+ },
+ "filter_chains": [
+ {
+ "filters": [
+ {
+ "name": "envoy.filters.network.http_connection_manager",
+ "typed_config": {
+ "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager",
+ "stat_prefix": "public_listener",
+ "route_config": {
+ "name": "public_listener",
+ "virtual_hosts": [
+ {
+ "name": "public_listener",
+ "domains": [
+ "*"
+ ],
+ "routes": [
+ {
+ "match": {
+ "prefix": "/"
+ },
+ "route": {
+ "cluster": "local_app",
+ "timeout": "60s"
+ <생략>
+ {
+ "route_config": {
+ "@type": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
+ "name": "public_listener",
+ "virtual_hosts": [
+ {
+ "name": "public_listener",
+ "domains": [
+ "*"
+ ],
+ "routes": [
+ {
+ "match": {
+ "prefix": "/"
+ },
+ "route": {
+ "cluster": "local_app",
+ "timeout": "60s"
+ <생략>
+
Case | Resources: CPU | Resources: Memory | Limits: CPU | Limits: Memory | Performance |
---|---|---|---|---|---|
1-1. Consul Default | 100 m | 100 Mi | 100 m | 100 Mi | 896 reqs / sec |
1-2. Istio Default | 10 m | 40 Mi | 2 C | 1 Gi | 1527 reqs / sec |
2-1. Consul w/ alloc | 250 m | 500 Mi | 250 m | 500 Mi | 1860 reqs / sec |
2-2. Consul resources like Istio | 10 m | 40 Mi | 2 C | 1 Gi | 3002 reqs / sec |
kubectl create namespace consul #consul namespace
+kubectl create namespace istio-system #istio namespace
+
global:
+ enabled: true
+ datacenter: dc1
+ logLevel: "debug"
+ logJSON: false
+ image: hashicorp/consul-enterprise:1.12.3-ent
+ gossipEncryption:
+ secretKey: key
+ secretName: consul-gossip-encryption-key
+ tls:
+ enabled: false
+ enableConsulNamespaces: true
+ imageEnvoy: envoyproxy/envoy:v1.22-latest
+ enterpriseLicense:
+ secretName: license
+ secretKey: key
+server:
+ enabled: true
+ replicas: 1
+ bootstrapExpect: 1
+ exposeGossipAndRPCPorts: true
+client:
+ enabled: true
+ extraConfig: |
+ {
+ "log_level": "DEBUG"
+ }
+ui:
+ enabled: true
+ service:
+ type: LoadBalancer
+connectInject:
+ enabled: true
+ default: true
+controller:
+ enabled: true
+ingressGateways:
+ enabled: true
+ defaults:
+ replicas: 1
+ service:
+ type: LoadBalancer
+ ports:
+ - port: 80
+
helm install consul hashicorp/consul --namespace consul --values values.yaml
+
# web-consul.yaml
+# kubectl apply -f web-consul.yaml -nconsul
+
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: web-consul
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: web-consul
+spec:
+ selector:
+ app: web-consul
+ ports:
+ - port: 9090
+ targetPort: 9090
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: web-deploy-consul
+ labels:
+ app: web-consul
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: web-consul
+ template:
+ metadata:
+ labels:
+ app: web-consul
+ annotations:
+ consul.hashicorp.com/connect-inject: 'true'
+ spec:
+ serviceAccountName: web-consul
+ containers:
+ - name: web-consul
+ image: nicholasjackson/fake-service:v0.23.1
+ ports:
+ - containerPort: 9090
+ env:
+ - name: 'LISTEN_ADDR'
+ value: '0.0.0.0:9090'
+ - name: 'NAME'
+ value: 'web'
+ - name: 'MESSAGE'
+ value: 'Hello World'
+
# proxy-defaults.yaml
+# kubectl apply -f proxy-defaults.yaml -nconsul
+
+apiVersion: consul.hashicorp.com/v1alpha1
+kind: ProxyDefaults
+metadata:
+ name: global
+spec:
+ config:
+ protocol: http
+
# service-defaults.yaml
+# kubectl apply -f service-defaults.yaml -nconsul
+
+apiVersion: consul.hashicorp.com/v1alpha1
+kind: ServiceDefaults
+metadata:
+ name: web-consul
+spec:
+ protocol: http
+
# ingress-gateway.yaml
+# kubectl apply -f ingresss-gateway.yaml -nconsul
+
+apiVersion: consul.hashicorp.com/v1alpha1
+kind: IngressGateway
+metadata:
+ name: ingress-gateway
+spec:
+ listeners:
+ - port: 80
+ protocol: http
+ services:
+ - name: web-consul
+
curl -sL https://istio.io/downloadIstioctl | sh -
+export PATH=$HOME/.istioctl/bin:$PATH
+
istioctl install
+
kubectl label namespace istio-system istio-injection=enabled
+
# web-istio.yaml
+# kubectl apply -f web-istio.yaml -nistio-system
+
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: web-istio
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: web-istio
+spec:
+ selector:
+ app: web-istio
+ ports:
+ - port: 9090
+ targetPort: 9090
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: web-deploy-istio
+ labels:
+ app: web-istio
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: web-istio
+ template:
+ metadata:
+ labels:
+ app: web-istio
+ spec:
+ serviceAccountName: web-istio
+ containers:
+ - name: web-istio
+ image: nicholasjackson/fake-service:v0.23.1
+ ports:
+ - containerPort: 9090
+ env:
+ - name: 'LISTEN_ADDR'
+ value: '0.0.0.0:9090'
+ - name: 'NAME'
+ value: 'web-istio'
+ - name: 'MESSAGE'
+ value: 'Hello World'
+
# ingress-gateway.yaml
+# kubectl apply -f ingress-gateway.yaml
+
+apiVersion: networking.istio.io/v1alpha3
+kind: Gateway
+metadata:
+ name: fakeservice-gateway
+ namespace: istio-system
+spec:
+ selector:
+ istio: ingressgateway
+ servers:
+ - port:
+ number: 80
+ name: http
+ protocol: HTTP
+ hosts:
+ - "*"
+
# virtual-service.yaml
+# kubectl apply -f virtual-service.yaml
+
+apiVersion: networking.istio.io/v1alpha3
+kind: VirtualService
+metadata:
+ name: web-istio
+ namespace: istio-system
+spec:
+ hosts:
+ - "*"
+ gateways:
+ - fakeservice-gateway
+ http:
+ - match:
+ - uri:
+ exact: "/"
+ route:
+ - destination:
+ host: web-istio
+ port:
+ number: 9090
+
테스트 결과 요약
Case | Resources: CPU | Resources: Memory | Limits: CPU | Limits: Memory | Performance |
---|---|---|---|---|---|
1-1. Consul Default | 100 m | 100 Mi | 100 m | 100 Mi | 896 reqs / sec |
1-2. Istio Default | 10 m | 40 Mi | 2 C | 1 Gi | 1527 reqs / sec |
2-1. Consul w/ alloc | 250 m | 500 Mi | 250 m | 500 Mi | 1860 reqs / sec |
2-2. same resources | 10 m | 40 Mi | 2 C | 1 Gi | 3002 reqs / sec |
# concurrent user: 100
+# total request: 15000
+
+hyungwook@MacBook-Pro ~ hey -n 15000 -c 100 http://20.200.225.63:8080/
+Summary:
+ Total: 16.7374 secs
+ Slowest: 0.2477 secs
+ Fastest: 0.0060 secs
+ Average: 0.1092 secs
+ Requests/sec: 896.1988
+ Total data: 3865847 bytes
+ Size/request: 257 bytes
+Response time histogram: 0.006 [1] |
+0.030 [828] |■■■■ 0.054 [100] |■
+0.079 [144] |■
+0.103 [7519] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 0.127 [4063] |■■■■■■■■■■■■■■■■■■■■■■
+0.151 [75] |
+0.175 [199] |■
+0.199 [1756] |■■■■■■■■■
+0.224 [230] |■
+0.248 [85] |
+Latency distribution:
+ 10% in 0.0855 secs
+ 25% in 0.0958 secs
+ 50% in 0.1014 secs
+ 75% in 0.1091 secs
+ 90% in 0.1866 secs
+ 95% in 0.1946 secs
+ 99% in 0.2027 secs
+Details (average, fastest, slowest):
+ DNS+dialup: 0.0001 secs, 0.0060 secs, 0.2477 secs
+ DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0000 secs
+ req write: 0.0000 secs, 0.0000 secs, 0.0052 secs
+ resp wait: 0.1090 secs, 0.0058 secs, 0.2282 secs
+ resp read: 0.0001 secs, 0.0000 secs, 0.0062 secs
+Status code distribution:
+ [200] 15000 responses
+
# concurrent user: 100
+# total request: 15000
+
+hyungwook@MacBook-Pro ~ hey -n 15000 -c 100 http://20.196.248.118/
+Summary:
+ Total: 9.8192 secs
+ Slowest: 0.2723 secs
+ Fastest: 0.0055 secs
+ Average: 0.0639 secs
+ Requests/sec: 1527.6172
+ Total data: 3804546 bytes
+ Size/request: 253 bytes
+Response time histogram:
+0.006 [1] |
+0.032 [1005] |■■■■■■■
+0.059 [6144] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 0.086 [5457] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 0.112 [1887] |■■■■■■■■■■■■
+0.139 [337] |■■ 0.166 [55] | 0.192 [5] | 0.219 [34] | 0.246 [60] | 0.272 [15] |
+Latency distribution:
+ 10% in 0.0360 secs
+ 25% in 0.0467 secs
+ 50% in 0.0603 secs
+ 75% in 0.0772 secs
+ 90% in 0.0940 secs
+ 95% in 0.1051 secs
+ 99% in 0.1447 secs
+Details (average, fastest, slowest):
+ DNS+dialup: 0.0002 secs, 0.0055 secs, 0.2723 secs
+ DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0000 secs
+ req write: 0.0000 secs, 0.0000 secs, 0.0063 secs
+ resp wait: 0.0636 secs, 0.0055 secs, 0.2723 secs
+ resp read: 0.0001 secs, 0.0000 secs, 0.0081 secs
+Status code distribution:
+ [200] 15000 responses
+
Consul w/ resource allocation: 1860 Requests / sec (Case 1-2. Istio 1527 Requests / sec 대비 약 22% 빠름 )
# kubectl edit deployments consul-client-ingress-gateway -nconsul
+# from 120th line..
+
+ name: ingress-gateway
+ ports:
+ - containerPort: 21000
+ name: gateway-health
+ protocol: TCP
+ - containerPort: 8080
+ name: gateway-0
+ protocol: TCP
+ - containerPort: 9090
+ name: gateway-1
+ protocol: TCP
+ readinessProbe:
+ failureThreshold: 3
+ initialDelaySeconds: 10
+ periodSeconds: 10
+ successThreshold: 1
+ tcpSocket:
+ port: 21000
+ timeoutSeconds: 5
+ resources:
+ limits:
+ cpu: 250m
+ memory: 500Mi
+ requests:
+ cpu: 250m
+ memory: 500Mi
+
# from 177th line..
+
+ name: consul-sidecar
+ resources:
+ limits:
+ cpu: 25m
+ memory: 500Mi
+ requests:
+ cpu: 250m
+ memory: 250Mi
+
hyungwook@MacBook-Pro ~ hey -n 15000 -c 100 http://20.200.225.63:8080/
+Summary:
+ Total: 8.0605 secs
+ Slowest: 0.2963 secs
+ Fastest: 0.0049 secs
+ Average: 0.0525 secs
+ Requests/sec: 1860.9347
+ Total data: 3867565 bytes
+ Size/request: 257 bytes
+Response time histogram:
+0.005 [1] |
+0.034 [3890] |■■■■■■■■■■■■■■■■■■■■■■■■
+0.063 [6524] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 0.092 [3937] |■■■■■■■■■■■■■■■■■■■■■■■■
+0.121 [496] |■■■
+0.151 [52] |
+0.180 [0] |
+0.209 [10] |
+0.238 [50] |
+0.267 [0] |
+0.296 [40] |
+Latency distribution:
+ 10% in 0.0237 secs
+ 25% in 0.0333 secs
+ 50% in 0.0506 secs
+ 75% in 0.0668 secs
+ 90% in 0.0803 secs
+ 95% in 0.0902 secs
+ 99% in 0.1224 secs
+Details (average, fastest, slowest):
+ DNS+dialup: 0.0001 secs, 0.0049 secs, 0.2963 secs
+ DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0000 secs
+ req write: 0.0000 secs, 0.0000 secs, 0.0165 secs
+ resp wait: 0.0521 secs, 0.0049 secs, 0.2771 secs
+ resp read: 0.0002 secs, 0.0000 secs, 0.0105 secs
+Status code distribution:
+ [200] 15000 responses
+
Istio 의 Resource Allocation 과 동률 구성: 3002 Requests / sec (Case 1-2. Istio 1527 Requests / sec 대비 약 2배 빠름 )
# kubectl edit deployment istio-ingressgateway -nistio-system
+# from 149th line..
+
+ resources:
+ limits:
+ cpu: "2"
+ memory: 1Gi
+ requests:
+ cpu: 10m
+ memory: 40Mi
+
+hyungwook@MacBook-Pro ~ hey -n 15000 -c 100 http://20.200.225.63:8080/
+Summary:
+ Total: 4.9955 secs
+ Slowest: 0.2424 secs
+ Fastest: 0.0059 secs
+ Average: 0.0322 secs
+ Requests/sec: 3002.6970
+ Total data: 3864684 bytes
+ Size/request: 257 bytes
+Response time histogram:
+0.006 [1] |
+0.030 [8452] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 0.053 [5382] |■■■■■■■■■■■■■■■■■■■■■■■■■
+0.077 [772] |■■■■
+0.100 [80] |
+0.124 [112] |■
+0.148 [86] |
+0.171 [111] |■
+0.195 [3] |
+0.219 [0] |
+0.242 [1] |
+Latency distribution:
+ 10% in 0.0170 secs
+ 25% in 0.0216 secs
+ 50% in 0.0278 secs
+ 75% in 0.0361 secs
+ 90% in 0.0490 secs
+ 95% in 0.0614 secs
+ 99% in 0.1331 secs
+Details (average, fastest, slowest):
+ DNS+dialup: 0.0001 secs, 0.0059 secs, 0.2424 secs
+ DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0000 secs
+ req write: 0.0000 secs, 0.0000 secs, 0.0068 secs
+ resp wait: 0.0319 secs, 0.0058 secs, 0.2424 secs
+ resp read: 0.0001 secs, 0.0000 secs, 0.0064 secs
+Status code distribution:
+ [200] 15000 responses
+
Jaeger 연동을 위해 Consul on K8s 환경을 구성합니다. 해당 가이드의 경우에는 여기를 참고하세요.
# license파일 생성
+vi consul.lic
+
+# 생성한 license파일로 secret 생성
+kubectl create secret generic license --from-file='key=./consul.lic'
+
GOSSIP_KEY="VeQ8CHV3sDY/bHCseXC7PGXNTSXtWWvOzQKAaFFo9oE="
+kubectl patch secret consul-gossip-encryption-key -n consul --patch='{"stringData":{"key": "$GOSSIP_KEY"}}'
+
values.yaml
파일 수정 및 배포합니다.
values.yaml
파일 예시global:
+ name: consul
+ datacenter: dc1
+ logLevel: "debug"
+ logJSON: false
+ image: hashicorp/consul-enterprise:1.12.3-ent
+ gossipEncryption:
+ autoGenerate: true
+ tls:
+ enabled: false
+ enableAutoEncrypt: false
+ verify: false
+ httpsOnly: false
+ imageEnvoy: envoyproxy/envoy:v1.22-latest
+ enterpriseLicense:
+ secretName: license
+ secretKey: key
+server:
+ replicas: 3
+client:
+ enabled: true
+ exposeGossipPorts: true
+ extraConfig: |
+ {
+ "log_level": "debug"
+ }
+ grpc: true
+ui:
+ enabled: true
+ service:
+ type: LoadBalancer
+connectInject:
+ enabled: true
+controller:
+ enabled: true
+ #terminatingGateways:
+ #enabled: true
+ #apiGateway:
+ #enabled: true
+ #image: "hashicorp/consul-api-gateway:latest"
+ingressGateways:
+ enabled: true
+ gateways:
+ - name: ingress-gateway
+ service:
+ type: LoadBalancer
+ ports:
+ - port: 5000
+
Jaeger를 설치할 때 cert-manager 설치가 필수적으로 요구됩니다.
Since version 1.31 the Jaeger Operator uses webhooks to validate Jaeger custom resources (CRs). This requires an installed version of the cert-manager.
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.9.1/cert-manager.yaml
+
kubectl get pods -n cert-manager
+NAME READY STATUS RESTARTS AGE
+cert-manager-6544c44c6b-z76nd 1/1 Running 0 25s
+cert-manager-cainjector-5687864d5f-pdzbn 1/1 Running 0 25s
+cert-manager-webhook-785bb86798-v6phx 1/1 Running 0 25s
+
Tracing을 위해 Jaeger 공식 문서를 참고하여 K8s 환경에 Jaeger Operator를 설치합니다.
💡참고 : 동일 네임스페이스 배포할 경우 해당 과정은 생략!
consul-jaeger
RoleBinding 생성# role-binding.yaml
+kind: RoleBinding
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+ name: jaeger-operator-in-myproject
+ namespace: consul-jaeger
+subjects:
+- kind: ServiceAccount
+ name: jaeger-operator
+ namespace: observability
+roleRef:
+ kind: Role
+ name: jaeger-operator
+ apiGroup: rbac.authorization.k8s.io
+
kubectl apply -f role-binding.yaml
+
Jaeger Operator를 배포하기 위한 observbility
네임스페이스를 생성합니다. 이때, 별도의 네임스페이스에 배포할 경우에는 다운받은 .yaml
에 설저된 네임스페이스명을 변경하셔야 합니다. 참고
kubectl create namespace observability
+kubectl create -f https://github.com/jaegertracing/jaeger-operator/releases/download/v1.37.0/jaeger-operator.yaml -n observability
+
jaeger-operator
확인kubectl get deployment jaeger-operator -n observability
+NAME READY UP-TO-DATE AVAILABLE AGE
+jaeger-operator 1/1 1 1 2m30s
+
실제 K8s 환경에서 Jaeger
리소스 생성을 위해 다음 .yaml
파일을 배포합니다. 본 문서에서는 편의상 AllInOne
이미지를 사용하여 배포합니다.
AllInOne
이미지는 프로덕션 환경에서 사용하기에는 적합하지 않으며, Dev 또는 Test 목적으로 사용해야 합니다. (배포전략 참고)
The simplest possible way to create a Jaeger instance is by creating a YAML file like the following example. This will install the default AllInOne strategy, which deploys the “all-in-one” image (agent, collector, query, ingester, Jaeger UI) in a single pod, using in-memory storage by default.
# simplest.yaml
+apiVersion: jaegertracing.io/v1
+kind: Jaeger
+metadata:
+ name: simplest
+ namespace: observability
+
# simplest-debug.yaml
+apiVersion: jaegertracing.io/v1
+kind: Jaeger
+metadata:
+ name: simplest
+ namespace: observability
+spec:
+ strategy: allInOne
+ allInOne:
+ image: jaegertracing/all-in-one:latest
+ options:
+ log-level: debug
+
kubectl apply -f simplest.yaml
+
{"level":"info","ts":1661997111.1498919,"caller":"healthcheck/handler.go:129","msg":"Health Check state change","status":"ready"}
로그를 통해서 정상적인 상태 확인됨kubectl logs -l app.kubernetes.io/instance=simplest -n consul-jaeger
+{"level":"info","ts":1661997111.149404,"caller":"channelz/funcs.go:340","msg":"[core][Channel #10] Channel Connectivity change to TRANSIENT_FAILURE","system":"grpc","grpc_log":true}
+{"level":"info","ts":1661997111.1495373,"caller":"app/static_handler.go:181","msg":"UI config path not provided, config file will not be watched"}
+{"level":"info","ts":1661997111.149864,"caller":"app/server.go:217","msg":"Query server started","http_addr":"[::]:16686","grpc_addr":"[::]:16685"}
+{"level":"info","ts":1661997111.1498919,"caller":"healthcheck/handler.go:129","msg":"Health Check state change","status":"ready"}
+{"level":"info","ts":1661997111.149912,"caller":"app/server.go:300","msg":"Starting GRPC server","port":16685,"addr":":16685"}
+{"level":"info","ts":1661997111.1499252,"caller":"channelz/funcs.go:340","msg":"[core][Server #9 ListenSocket #12] ListenSocket created","system":"grpc","grpc_log":true}
+{"level":"info","ts":1661997111.1499453,"caller":"app/server.go:281","msg":"Starting HTTP server","port":16686,"addr":":16686"}
+{"level":"info","ts":1661997112.150468,"caller":"channelz/funcs.go:340","msg":"[core][Channel #10 SubChannel #11] Subchannel Connectivity change to IDLE","system":"grpc","grpc_log":true}
+{"level":"info","ts":1661997112.1505697,"caller":"grpclog/component.go:71","msg":"[core]pickfirstBalancer: UpdateSubConnState: 0xc00082a700, {IDLE connection error: desc = \"transport: Error while dialing dial tcp :16685: connect: connection refused\"}","system":"grpc","grpc_log":true}
+{"level":"info","ts":1661997112.1505857,"caller":"channelz/funcs.go:340","msg":"[core][Channel #10] Channel Connectivity change to IDLE","system":"grpc","grpc_log":true}
+
Jaeger Auto Injection 및 Manaul Injection 활용방안을 가이드합니다. (3)에서 Jaeger
리소스를 직접 배포했다면 생략하셔도 됩니다.
해당 방안은 3)-(3)에서 작성한 방식으로, 관리되는 네임스페이스에 있는 애플리케이션의 tracing을 수행합니다.
해당 방안은 annotation 절에 "sidecar.jaegertracing.io/inject": "true"
를 기입하여 tracing 하고자 하는 애플리케이션에 sidecar auto-injection을 수행합니다.
metadata:
+ name: web-deployment
+ labels:
+ app: web
+ namespace: observability
+ annotations:
+ "sidecar.jaegertracing.io/inject": 'true'
+
tracing 하고자 하는 애플리케이션에 직접 sidecar를 붙혀 tracing 합니다.
- name: jaeger-agent
+ image: jaegertracing/jaeger-agent:latest
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 5775
+ name: zk-compact-trft
+ protocol: UDP
+ - containerPort: 5778
+ name: config-rest
+ protocol: TCP
+ - containerPort: 6831
+ name: jg-compact-trft
+ protocol: UDP
+ - containerPort: 6832
+ name: jg-binary-trft
+ protocol: UDP
+ - containerPort: 14271
+ name: admin-http
+ protocol: TCP
+ args:
+ - --reporter.grpc.host-port=dns:///simplest-collector-headless.observability:14250
+ - --reporter.type=grpc
+
기본적을 Jaeger UI는 ClusterIP
로 배포됩니다. 외부에서 접속하기 위해 다음 몇 가지 방안을 제시합니다.
참고 : 본 문서에서는 편의상 LoadBalancer 타입으로 변경하는 샘플을 제공합니다.
기본적으로 Jaeager UI는 16686 Port를 사용합니다. 필자는 편읜상 simplest-query
서비스를 LoadBalancer
타입으로 변경하여 조회합니다.
spec:
+(중략)
+ ports:
+ - name: http-query
+ nodePort: 32731
+ port: 16686
+ protocol: TCP
+ targetPort: 16686
+ - name: grpc-query
+ nodePort: 31322
+ port: 16685
+ protocol: TCP
+ targetPort: 16685
+ selector:
+ app: jaeger
+ app.kubernetes.io/component: all-in-one
+ app.kubernetes.io/instance: simplest
+ app.kubernetes.io/managed-by: jaeger-operator
+ app.kubernetes.io/name: simplest
+ app.kubernetes.io/part-of: jaeger
+ sessionAffinity: None
+ type: LoadBalancer
+
apiVersion: consul.hashicorp.com/v1alpha1
+kind: IngressGateway
+metadata:
+ name: ingress-gateway
+ namespace: consul
+spec:
+ listeners:
+ - port: 5000
+ protocol: http
+ services:
+ - name: web
+ hosts: ['*']
+
apiVersion: consul.hashicorp.com/v1alpha1
+kind: ServiceDefaults
+metadata:
+ name: web
+spec:
+ protocol: "http"
+
ProxyDefaults
설정을 통해 Collector 서버에 대한 주소 및 Clustesr Name에 대해 선언합니다.
apiVersion: consul.hashicorp.com/v1alpha1
+kind: ProxyDefaults
+metadata:
+ name: global
+ namespace: consul
+spec:
+ config:
+ protocol: http
+ envoy_tracing_json: |
+ {
+ "http":{
+ "name":"envoy.tracers.zipkin",
+ "typedConfig":{
+ "@type":"type.googleapis.com/envoy.config.trace.v3.ZipkinConfig",
+ "collector_cluster":"simplest-collector",
+ "collector_endpoint_version":"HTTP_JSON",
+ "collector_endpoint":"/api/v2/spans",
+ "shared_span_context":false
+ }
+ }
+ }
+ envoy_extra_static_clusters_json: |
+ {
+ "connect_timeout":"3.000s",
+ "dns_lookup_family":"V4_ONLY",
+ "lb_policy":"ROUND_ROBIN",
+ "load_assignment":{
+ "cluster_name":"simplest-collector",
+ "endpoints":[
+ {
+ "lb_endpoints":[
+ {
+ "endpoint":{
+ "address":{
+ "socket_address":{
+ "address":"simplest-collector",
+ "port_value":9411,
+ "protocol":"TCP"
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "name":"simplest-collector",
+ "type":"STRICT_DNS"
+ }
+
ingress gateway + tracing 고려사항 : https://developer.hashicorp.com/consul/docs/connect/distributed-tracing#considerations
jaeger 배포 helm chart : https://git.app.uib.no/caleno/helm-charts/-/tree/597accc8e61dfb3a78f2e4f1b9622c8d3f32b4f2/stable/jaeger-operator/templates
nomad namespace apply -description "Boundary" boundary
+
job "postgresql" {
+ type = "service"
+ datacenters = ["hashistack"]
+ namespace = "boundary"
+
+ group "postgres" {
+ count = 1
+
+ volume "postgres-vol" {
+ type = "host"
+ read_only = false
+ source = "postgres-vol"
+ }
+
+ task "db" {
+ driver = "docker"
+
+ volume_mount {
+ volume = "postgres-vol"
+ destination = "/var/lib/postgresql/data"
+ read_only = false
+ }
+
+ config {
+ image = "postgres:13.2"
+ port_map {
+ pg = 5432
+ }
+ }
+
+ env {
+ POSTGRES_PASSWORD = "postgres"
+ POSTGRES_USER = "postgres"
+ PGDATA = "/var/lib/postgresql/data/pgdata"
+ }
+
+ resources {
+ memory = 1024
+
+ network {
+ port "pg" {
+ static = 5432
+ }
+ }
+ }
+
+ service {
+ name = "postgresql"
+ tags = ["db", "boundary"]
+
+ port = "pg"
+
+ check {
+ type = "tcp"
+ interval = "10s"
+ timeout = "2s"
+ port = "pg"
+ }
+ }
+ }
+ }
+}
+
nomad job run -namespace="boundary" postgresql.nomad
+
# Login
+psql -h 172.28.128.11 -U postgres postgres
+
# <enter the password> postgres
+CREATE ROLE boundary WITH LOGIN PASSWORD 'PASSWORD';
+CREATE DATABASE boundary OWNER boundary;
+GRANT ALL PRIVILEGES ON DATABASE boundary TO boundary;
+ALTER USER boundary PASSWORD 'boundary';
+
</tmp/config.hcl>
disable_mlock = true
+
+controller {
+ name = "controller-0"
+ database {
+ url = "postgresql://boundary:boundary@172.28.128.11:5432/boundary?sslmode=disable"
+ }
+}
+
+kms "aead" {
+ purpose = "root"
+ aead_type = "aes-gcm"
+ key = "sP1fnF5Xz85RrXyELHFeZg9Ad2qt4Z4bgNHVGtD6ung="
+ key_id = "global_root"
+}
+
+kms "aead" {
+ purpose = "worker-auth"
+ aead_type = "aes-gcm"
+ key = "8fZBjCUfN0TzjEGLQldGY4+iE9AkOvCfjh7+p0GtRBQ="
+ key_id = "global_worker-auth"
+}
+
+kms "aead" {
+ purpose = "recovery"
+ aead_type = "aes-gcm"
+ key = "8fZBjCUfN0TzjEGLQldGY4+iE9AkOvCfjh7+p0GtRBQ="
+ key_id = "global_recovery"
+}
+
boundary database init -config=/tmp/config.hcl
+
locals {
+ version = "0.6.2"
+ postgre_ip = "172.28.128.11"
+ postgre_port = "5432"
+}
+
+job "boundary-controller" {
+ type = "service"
+ datacenters = ["hashistack"]
+ namespace = "boundary"
+
+ group "controller" {
+ count = 1
+
+ network {
+ mode = "host"
+ }
+
+ task "migration" {
+ driver = "raw_exec"
+
+ env {
+ BOUNDARY_POSTGRES_URL = "postgresql://boundary:boundary@${local.postgre_ip}:${local.postgre_port}/boundary?sslmode=disable"
+ }
+ artifact {
+ source = "https://releases.hashicorp.com/boundary/${local.version}/boundary_${local.version}_linux_amd64.zip"
+ }
+ template {
+ data = <<EOH
+disable_mlock = true
+
+{{ range service "postgresql" }}
+controller {
+ name = "controller-0"
+ database {
+ url = "postgresql://boundary:boundary@{{ .Address }}:{{ .Port }}/boundary?sslmode=disable"
+ }
+}
+{{ end }}
+
+listener "tcp" {
+ address = "0.0.0.0:9200"
+ purpose = "api"
+ tls_disable = true
+}
+listener "tcp" {
+ address = "0.0.0.0:9201"
+ purpose = "cluster"
+ tls_disable = true
+}
+
+kms "aead" {
+ purpose = "root"
+ aead_type = "aes-gcm"
+ key = "sP1fnF5Xz85RrXyELHFeZg9Ad2qt4Z4bgNHVGtD6ung="
+ key_id = "global_root"
+}
+
+kms "aead" {
+ purpose = "worker-auth"
+ aead_type = "aes-gcm"
+ key = "8fZBjCUfN0TzjEGLQldGY4+iE9AkOvCfjh7+p0GtRBQ="
+ key_id = "global_worker-auth"
+}
+
+kms "aead" {
+ purpose = "recovery"
+ aead_type = "aes-gcm"
+ key = "8fZBjCUfN0TzjEGLQldGY4+iE9AkOvCfjh7+p0GtRBQ="
+ key_id = "global_recovery"
+}
+EOH
+ destination = "local/config/config.hcl"
+ }
+ config {
+ command = "local/boundary"
+ args = ["database", "migrate", "-config", "local/config/config.hcl"]
+ }
+ lifecycle {
+ hook = "prestart"
+ sidecar = false
+ }
+ }
+
+ task "controller" {
+ driver = "docker"
+
+ config {
+ image = "hashicorp/boundary:${local.version}"
+ port_map {
+ controller = 9200
+ cluster = 9201
+ }
+ mount {
+ type = "bind"
+ source = "local/config"
+ target = "/boundary"
+ }
+ // network_mode = "boundary-net"
+ // network_aliases = [
+ // "boundary-controller"
+ // ]
+ }
+
+ template {
+ data = <<EOH
+disable_mlock = true
+
+{{ range service "postgresql" }}
+controller {
+ name = "controller-0"
+ database {
+ url = "postgresql://boundary:boundary@{{ .Address }}:{{ .Port }}/boundary?sslmode=disable"
+ }
+ public_cluster_addr = "{{ env "NOMAD_ADDR_cluster" }}"
+}
+{{ end }}
+
+listener "tcp" {
+ address = "0.0.0.0:9200"
+ purpose = "api"
+ tls_disable = true
+}
+listener "tcp" {
+ address = "0.0.0.0:9201"
+ purpose = "cluster"
+ tls_disable = true
+}
+
+kms "aead" {
+ purpose = "root"
+ aead_type = "aes-gcm"
+ key = "sP1fnF5Xz85RrXyELHFeZg9Ad2qt4Z4bgNHVGtD6ung="
+ key_id = "global_root"
+}
+
+kms "aead" {
+ purpose = "worker-auth"
+ aead_type = "aes-gcm"
+ key = "8fZBjCUfN0TzjEGLQldGY4+iE9AkOvCfjh7+p0GtRBQ="
+ key_id = "global_worker-auth"
+}
+
+kms "aead" {
+ purpose = "recovery"
+ aead_type = "aes-gcm"
+ key = "8fZBjCUfN0TzjEGLQldGY4+iE9AkOvCfjh7+p0GtRBQ="
+ key_id = "global_recovery"
+}
+EOH
+ destination = "local/config/config.hcl"
+ }
+
+ env {
+ // BOUNDARY_POSTGRES_URL = "postgresql://boundary:boundary@${local.postgre_ip}:${local.postgre_port}/boundary?sslmode=disable"
+ SKIP_SETCAP = true
+ }
+
+ resources {
+ cpu = 300
+ memory = 500
+ network {
+ port "controller" {
+ static = 9200
+ }
+ port "cluster" {
+ static = 9201
+ }
+ }
+ }
+
+ service {
+ name = "boundary"
+ tags = ["cluster"]
+
+ port = "cluster"
+
+ check {
+ type = "tcp"
+ interval = "10s"
+ timeout = "2s"
+ port = "cluster"
+ }
+ }
+ }
+ }
+}
+
nomad job run -namespace="boundary" boundary-controller.nomad
+
locals {
+ version = "0.6.2"
+}
+
+job "boundary-worker" {
+ type = "service"
+ datacenters = ["hashistack"]
+ namespace = "boundary"
+
+ group "worker" {
+ count = 1
+
+ scaling {
+ enabled = true
+ min = 1
+ max = 3
+ }
+
+ network {
+ mode = "host"
+ }
+
+ task "worker" {
+ driver = "docker"
+
+ config {
+ image = "hashicorp/boundary:${local.version}"
+ port_map {
+ proxy = 9202
+ }
+ volumes = [
+ "local/boundary:/boundary/",
+ ]
+ // network_mode = "boundary-net"
+ }
+
+ template {
+ data = <<EOH
+disable_mlock = true
+
+listener "tcp" {
+ address = "0.0.0.0:9202"
+ purpose = "proxy"
+ tls_disable = true
+}
+
+worker {
+ name = "worker-0"
+ controllers = [
+{{ range service "boundary" }}
+ "{{ .Address }}",
+{{ end }}
+ ]
+ public_addr = "{{ env "NOMAD_ADDR_proxy" }}"
+}
+
+kms "aead" {
+ purpose = "root"
+ aead_type = "aes-gcm"
+ key = "sP1fnF5Xz85RrXyELHFeZg9Ad2qt4Z4bgNHVGtD6ung="
+ key_id = "global_root"
+}
+
+kms "aead" {
+ purpose = "worker-auth"
+ aead_type = "aes-gcm"
+ key = "8fZBjCUfN0TzjEGLQldGY4+iE9AkOvCfjh7+p0GtRBQ="
+ key_id = "global_worker-auth"
+}
+
+kms "aead" {
+ purpose = "recovery"
+ aead_type = "aes-gcm"
+ key = "8fZBjCUfN0TzjEGLQldGY4+iE9AkOvCfjh7+p0GtRBQ="
+ key_id = "global_recovery"
+}
+EOH
+ destination = "/local/boundary/config.hcl"
+ }
+
+ env {
+ // BOUNDARY_POSTGRES_URL = "postgresql://boundary:boundary@172.28.128.11:5432/boundary?sslmode=disable"
+ SKIP_SETCAP = true
+ }
+
+ resources {
+ network {
+ port "proxy" {}
+ }
+ }
+ }
+ }
+}
+
nomad job run -namespace="boundary" boundary-worker.nomad
+
locals {
+ version = "0.8.1"
+ private_ip = "192.168.0.27"
+ public_ip = "11.129.13.30"
+}
+
+job "boundary-dev" {
+ type = "service"
+ datacenters = ["home"]
+ namespace = "boundary"
+
+ constraint {
+ attribute = "${attr.os.name}"
+ value = "raspbian"
+ }
+
+ group "dev" {
+ count = 1
+
+ ephemeral_disk { sticky = true }
+
+ network {
+ mode = "host"
+ port "api" {
+ static = 9200
+ to = 9200
+ }
+ port "cluster" {
+ static = 9201
+ to = 9201
+ }
+ port "worker" {
+ static = 9202
+ to = 9202
+ }
+ }
+
+ task "dev" {
+ driver = "raw_exec"
+
+ env {
+ BOUNDARY_DEV_CONTROLLER_API_LISTEN_ADDRESS = local.private_ip
+ BOUNDARY_DEV_CONTROLLER_CLUSTER_LISTEN_ADDRESS = "0.0.0.0"
+ BOUNDARY_DEV_WORKER_PUBLIC_ADDRESS = local.public_ip
+ BOUNDARY_DEV_WORKER_PROXY_LISTEN_ADDRESS = local.private_ip
+ BOUNDARY_DEV_PASSWORD = "password"
+ }
+
+ // artifact {
+ // source = "https://releases.hashicorp.com/boundary/${local.version}/boundary_${local.version}_linux_arm.zip"
+ // }
+
+ config {
+ command = "boundary"
+ args = ["dev"]
+ }
+
+ resources {
+ cpu = 500
+ memory = 500
+ }
+
+ service {
+ name = "boundary"
+ tags = ["cluster"]
+
+ port = "cluster"
+
+ check {
+ type = "tcp"
+ interval = "10s"
+ timeout = "2s"
+ port = "api"
+ }
+ }
+ }
+ }
+}
+
-worker-public-addr
flag)provider "boundary" {
+ addr = "http://172.28.128.11:9200"
+// recovery_kms_hcl = <<EOT
+// kms "aead" {
+// purpose = "recovery"
+// aead_type = "aes-gcm"
+// key = "8fZBjCUfN0TzjEGLQldGY4+iE9AkOvCfjh7+p0GtRBQ="
+// key_id = "global_recovery"
+// }
+// EOT
+ auth_method_id = "ampw_U6FXouWRDK"
+ password_auth_method_login_name = "admin"
+ password_auth_method_password = "POByMKtvabYS1wtRHLgZ"
+}
+
+resource "boundary_scope" "global" {
+ global_scope = true
+ scope_id = "global"
+ description = "Global scope"
+}
+
+// Scope HashiStack
+resource "boundary_scope" "corp" {
+ name = "hashistack"
+ description = "hashistack scope"
+ scope_id = boundary_scope.global.id
+ auto_create_admin_role = true
+ auto_create_default_role = true
+}
+
+resource "boundary_auth_method" "corp_password" {
+ name = "corp_password_auth_method"
+ description = "Password auth method"
+ type = "password"
+ scope_id = boundary_scope.corp.id
+}
+
+resource "boundary_account" "user" {
+ for_each = var.users
+ name = each.key
+ description = "User account for my user"
+ type = "password"
+ login_name = lower(each.key)
+ password = "password"
+ auth_method_id = boundary_auth_method.corp_password.id
+}
+
+resource "boundary_user" "users" {
+ for_each = var.users
+ name = each.key
+ description = "User resource for ${each.key}"
+ account_ids = ["${boundary_account.user[each.key].id}"]
+ scope_id = boundary_scope.corp.id
+}
+
+resource "boundary_group" "admin" {
+ name = "admin"
+ description = "Organization group for readonly users"
+ member_ids = [for user in boundary_user.users : user.id]
+ scope_id = boundary_scope.corp.id
+}
+
+resource "boundary_user" "readonly_users" {
+ for_each = var.readonly_users
+ name = each.key
+ description = "User resource for ${each.key}"
+ scope_id = boundary_scope.corp.id
+}
+
+resource "boundary_group" "readonly" {
+ name = "read-only"
+ description = "Organization group for readonly users"
+ member_ids = [for user in boundary_user.readonly_users : user.id]
+ scope_id = boundary_scope.corp.id
+}
+
+resource "boundary_role" "corp_admin" {
+ name = "corp_admin"
+ description = "Corp Administrator role"
+ principal_ids = concat(
+ [for user in boundary_user.users: user.id]
+ )
+ grant_strings = ["id=*;type=*;actions=create,read,update,delete"]
+ scope_id = boundary_scope.corp.id
+}
+
+resource "boundary_role" "organization_readonly" {
+ name = "Read-only"
+ description = "Read-only role"
+ principal_ids = [boundary_group.readonly.id]
+ grant_strings = ["id=*;type=*;actions=read"]
+ scope_id = boundary_scope.corp.id
+}
+
+resource "boundary_scope" "core_infra" {
+ name = "core_infra"
+ description = "My first project!"
+ scope_id = boundary_scope.corp.id
+ auto_create_admin_role = true
+}
+
+resource "boundary_host_catalog" "backend_servers" {
+ name = "backend_servers"
+ description = "Backend servers host catalog"
+ type = "static"
+ scope_id = boundary_scope.core_infra.id
+}
+
+resource "boundary_host" "ssh_servers" {
+ for_each = var.ssh_server_ips
+ type = "static"
+ name = "ssh_server_service_${each.value}"
+ description = "ssh server host"
+ address = each.key
+ host_catalog_id = boundary_host_catalog.backend_servers.id
+}
+
+resource "boundary_host" "backend_servers" {
+ for_each = var.backend_server_ips
+ type = "static"
+ name = "backend_server_service_${each.value}"
+ description = "Backend server host"
+ address = each.key
+ host_catalog_id = boundary_host_catalog.backend_servers.id
+}
+
+resource "boundary_host_set" "ssh_servers" {
+ type = "static"
+ name = "ssh_servers"
+ description = "Host set for ssh servers"
+ host_catalog_id = boundary_host_catalog.backend_servers.id
+ host_ids = [for host in boundary_host.ssh_servers : host.id]
+}
+
+resource "boundary_host_set" "backend_servers" {
+ type = "static"
+ name = "backend_servers"
+ description = "Host set for backend servers"
+ host_catalog_id = boundary_host_catalog.backend_servers.id
+ host_ids = [for host in boundary_host.backend_servers : host.id]
+}
+
+# create target for accessing backend servers on port :8000
+resource "boundary_target" "backend_servers_service" {
+ type = "tcp"
+ name = "backend_server"
+ description = "Backend service target"
+ scope_id = boundary_scope.core_infra.id
+ default_port = "8080"
+
+ host_set_ids = [
+ boundary_host_set.backend_servers .id
+ ]
+}
+
+# create target for accessing backend servers on port :22
+resource "boundary_target" "backend_servers_ssh" {
+ type = "tcp"
+ name = "ssh_server"
+ description = "Backend SSH target"
+ scope_id = boundary_scope.core_infra.id
+ // default_port = "22"
+
+ host_set_ids = [
+ boundary_host_set.ssh_servers.id
+ ]
+}
+
+// anonymous
+resource "boundary_role" "global_anon_listing" {
+ scope_id = boundary_scope.global.id
+ grant_strings = [
+ "id=*;type=auth-method;actions=list,authenticate",
+ "type=scope;actions=list",
+ "id={{account.id}};actions=read,change-password"
+ ]
+ principal_ids = ["u_anon"]
+}
+
+resource "boundary_role" "org_anon_listing" {
+ scope_id = boundary_scope.corp.id
+ grant_strings = [
+ "id=*;type=auth-method;actions=list,authenticate",
+ "type=scope;actions=list",
+ "id={{account.id}};actions=read,change-password"
+ ]
+ principal_ids = ["u_anon"]
+}
+
+output "corp_auth_method_id" {
+ value = "boundary authenticate password -auth-method-id ${boundary_auth_method.corp_password.id} -login-name ${boundary_account.user["gslee"].login_name} -password ${boundary_account.user["gslee"].password}"
+}
+
variable "addr" {
+ default = "http://172.28.128.11:9200"
+}
+
+variable "users" {
+ type = set(string)
+ default = [
+ "gslee",
+ "Jim",
+ "Mike",
+ "Todd",
+ "Jeff",
+ "Randy",
+ "Susmitha"
+ ]
+}
+
+variable "readonly_users" {
+ type = set(string)
+ default = [
+ "Chris",
+ "Pete",
+ "Justin"
+ ]
+}
+
+variable "ssh_server_ips" {
+ type = set(string)
+ default = [
+ "172.28.128.11"
+ ]
+}
+
+variable "backend_server_ips" {
+ type = set(string)
+ default = [
+ "172.28.128.11",
+ "172.28.128.50",
+ "172.28.128.60",
+ "172.28.128.61",
+ "172.28.128.70",
+ ]
+}
+
기본 설정시 1,209,600초(2주)의 유효 기간을 갖게 되며, 설정에 따라 긴 유효시간의 적용이 가능합니다. (옵션 : deault_tls_client_ttl
)
설정은 상기 도식화한 절차 중 "2. kmip 기본 config" 단계에 적용 가능하며. 이는 KMIP 적용 흐름도
의 "4. kmip scope, role 정의" 단계에서 override 할 수 있습니다.
# 예시 - 1년의 유효기간을 설정
+vault write kmip/config \
+ listen_addrs=0.0.0.0:5696 \
+ tls_ca_key_type="rsa" \
+ tls_ca_key_bits=2048 \
+ server_ips=192.168.1.101,192.168.1.102 \
+ default_tls_client_ttl=31536000
+
일반적인 설정은 CA가 변경되지 않으나, CA에 직접적인 옵션 즉, 키 강도, 키 길이, 알고리즘 등을 변경하시면 CA가 재발급되므로 이 경우 영향이 있습니다.server_ip
, server_hostnames
를 추가하는 것으로는 CA는 변경되지 않습니다. 이는 SAN 인증서 발급에 영향이지만, vault에서 server인증서를 외부로 export 하는 과정이 없기 때문입니다.
#키강도 변경
+vault write kmip/config \
+ listen_addrs=0.0.0.0:5696 \
+ tls_ca_key_type="rsa" \
+ tls_ca_key_bits=4096
+
server_hostnames
기입 정보https 로 호출이 가능한 명칭이면 상관없습니다. dns로 호출시에는 네임서버의 zone의 설정에 따라 다를터 이나 fqdn으로 기입, 혹은 https 프로토콜로 호출만 가능하다면 호스트네임이어도 됩니다.
주의할 점은 http(s) 프로토콜로 호출시에 request/response 헤더에 요청될 host 명과 이것이 인증서의 CN(혹은 SAN 확장 필드)와 매칭되는지의 여부 입니다.
servername_hostnames
에 기입되어야하는 서버의 리스트 정보Vault 서버 정보를 기입해주시면 됩니다. (e.g. vault.mysite.com
)
주의
vault의 KMIP 통신에 앞서 server-client
의 상호 인증인 two way ssl
과정을 거치므로 각자의 cn을 판단하기 위해 개별 대상의 기입이 필요할 것으로 보여지나, operation role 부여시에 해당 권한에 대한 제어가 수행되므로 Client 의 값을 기입할 필요는 없습니다.
기존 인증서를 강제로 revoke 시키지 않으면 유효기간(ttl)동안 문제 없습니다.
KMIP이 활성화된 secrets engine이 다르다면, port는 활성화된 엔진 마다 개별 바인딩이 필요합니다.
Vault는 KMIP 으로 발급되는 키의 라이프사이클에 대한 제어권을 클라이언트에 포괄적으로 위임하고 있습니다.
scope 내의 Role을 부여하는 과정 중 Role에 operation_
접두사로 시작되는 권한을 부여하여, 해당 role에서 발급한 credential을 통해 해당 role에 부여된 KMIP에 대한 권한을 행사합니다. 요약하자면, Vault서버에서 KMIP자체의 라이프사이클이 아닌 KMIP의 권한을 부여하는 우회적인 관리책을 제공하고 있습니다.
따라서, 해당 role에서 KMIP 키에 대해 별다른 작업을 요청하지 않는다면, vault 내에서 키를 만료 혹은 수정(rotation)하는 작업을 수행하지 않으며,
해당 키는 지속적으로 사용가능합니다.
현 버전 vault의 KMIP에 대한 지원 정보는 현재 KMIP profile 1.4 표준을 따르고 있음이 퍼블릭 문서로 오픈되어있습니다.
(참고 - vault 가 구현중인 KMIP profile 정보 : https://developer.hashicorp.com/vault/docs/secrets/kmip-profiles)
Vault의 로그는 Vault 플랫폼 자체에 대한 이벤트 기록 만을 수행하여, 세밀한 키 처리에 대한 부분은 audit log
라는 기능을 enable 함으로써 이벤트 처리를 모니터링 할수 있습니다.
어떠한 추가 설정이 없다면, 기본적으로 시스템 로그에 기록되도록 되어있습니다. 말씀하신대로 해당 로그 파일을 별도 분리가능하며, 로테이션에 대한 설정도 용량/시간/갯수의 제한을 설정가능합니다.
(참고 : https://developer.hashicorp.com/vault/docs/configuration#log_rotate_duration)
해당 동작은 licence 정책에 따라 분화됩니다. 현재 라이선스로는 Read/Write 모두를 리더 노드가 처리하도록 되어있고. 요청을 처리받은 follwer 노드는 처리를 리더 노드로 포워딩합니다.
Read/Write를 모든 Vault에서 처리하는 동작은 Read Replica
라는 동작으로 Vault Enterprise의 Performance 라이선스에 해당됩니다.
만약 KMIP 사용이 가능한 라이선스인 ADP-KM 만 적용 된 경우, R/W가 빈번하지 않지만, 높은 비용의 라이선스 정책에서만 사용가능하던 ADP(Advanced Data Protection)라이선스의 KMIP 기능을 좀 더 비용합리적으로 사용가능하게끔, 해당 기능을 제거하고 좀 더 낮은 비용으로 책정된 라이선스입니다.
(현재 라이선스 정책에 대한 상세 참조 링크 : https://developer.hashicorp.com/vault/docs/enterprise/license/faq#q-how-can-the-new-adp-modules-be-purchased-and-what-features-are-customer-entitled-to-as-part-of-that-purchase)
vault에서 step-down
이라는 작업으로 칭합니다. 이때, 리더를 확인하여 다른 노드로 리더 권한을 이전하는 작업이 필요합니다.
해당 작업이 없이 리더를 shutdown하게 되면 자체적인 리더 선출과정이 발생하여 약 5~10초정도의 순단이 발생가능하기 때문에. maintanance 작업전엔 leader
를 확인 후 작업의 선행이 꼭 필요하겠습니다.
이미 사용중인 경우, 다음과 같이 진행됩니다.
다음은 구성된 Vault 클러스터 예시 이며, health check config는 이해를 돕기위한 설정 입니다.
작업간 rolling update 형태로 노드를 순환적 재기동하게되는데, LB의 health check
인터벌 때문에 해당노드가 다운되었음에도 노드를 신뢰하여 포워딩 하는 구간이 발생하게 됩니다.
vault follower node의 확인
작업 대상인 follower node를 GSLB 타겟에서 제거
2.1 제거된 follower node를 셧다운 후 키 마이그레이션 작업 후 GSLB의 타겟에 추가
다음 follower node에 대해 "2~3"작업 반복
3.1. follower node 기동 후 정상적인 follower로서 join 되었는지 확인
leader node를 GSLB 타겟에서 제거
4-1. 제거된 leader node를 "step-down"하여 leader role을 follower로 마이그레이션
4-2. "3"의 과정을 현재 node에서 수행
가능한 경우 KMIP 클라이언트 (e.g.MongoDB)를 재기동 하여 정상적으로 구동됨 확인
4의 과정에서 taget을 먼저 제거하는 이유는, step-down시 ready 상태가 되는 leader노드로 LB가 요청을 보낼시 역시 50X대 서버 에러를 리턴받기 때문입니다.
follower를 통해 유입된 reqeust는 response또한 follower를 통해 회신되므로, 요청처리에는 문제가 없습니다
systemd 상에 등록되어 있는 vault service는 "vault server" command를 사용합니다.
systemd로 등록된 서비스는 따로 설정해 주지 않는다면 Standart Out,Error 모두 syslog에 저장하므로, Vault Service Script에서 vault log가 저장되지 않도록 Standard Error를 null로 처리하고, vault server
command에 옵션을 주어 vault log를 외부로 지정할 수 있습니다.
# /lib/systemd/system/vault.service 수정
+[Unit]
+...
+
+[Service]
+...
+ExecStart=/usr/local/bin/vault server -config=/etc/vault.d/vault.hcl -log-file={ Log_path }
+StandardError=null
+
+[Install]
+...
+
ExecStart
항목에 -log-file
항목 추가StandardError=null
항모고 추가pkill -HUP vault
커맨드실행하는 경우에 vault 프로세스가 재시작되면서 Seal(봉인) 상태가 될까요?해당 pkill -HUP vault 명령어는 vault 가 재기동되는 명령어가 아닌 설정을 reload하는 명령어로 Vault seal에는 영향이 없습니다.
Vault service 상에서도 명시적으로 Reload 기능이 있으며, Systemctl reload vault.service
명령으로 같은 기능을 수행할 수 있습니다.
# /lib/systemd/system/vault.service 수정
+[Unit]
+...
+
+[Service]
+...
+ExecReload=/bin/kill --signal HUP $MAINPID
+
+[Install]
+...
+
Vault KMIP Secret Engine에서의 서버측 SSL 인증서의 경우 하단 3가지 상황에서 갱신됩니다
PR 기능을 포함된 라이선스 사용 시 PR이 없는 (e.g. ADP-KM) Vault License로 교체하면 Performance Standby 상태가 종료되면서 vault 리더 노드의 프로세스 리로드 시 팔로워 노드 프로세스가 재시작되어 seal 상태가 되는 상황이 발생합니다.
이 경우 하단 링크와 같이 vault config 파일에 disable_performance_standby=true
를 명시적으로 기입함으로써 해당 문제를 방지할 수 있습니다.
--kmipKeyStatePollingSeconds
옵션 동작MongoDB의 --kmipKeyStatePollingSeconds
옵션은 MongoDB 에서 Vault Kmip 으로 암호화 Key가 Active 상태인지 확인하는 주기 입니다.
https://learn.hashicorp.com/tutorials/vault/reference-architecture#network-connectivity
127.0.0.1:8200
127.0.0.1:8201
Source | Destination | port | protocol | Direction | Purpose |
---|---|---|---|---|---|
외부 호출지점에서 | Vault 서버로 | 8200 | tcp | 인바운드 | Vault API |
Vault 서버 에서 | Vault 서버로 | 8200 | tcp | 양방향 | Cluster bootstrapping |
Vault 서버 에서 | Vault 서버로 | 8201 | tcp | 양방향 | Raft, replication, request forwarding |
listener "tcp" {
+ address = "127.0.0.1:8200"
+}
+
+listener "tcp" {
+ address = "10.0.0.5:8200"
+}
+
+# Advertise the non-loopback interface
+api_addr = "https://10.0.0.5:8200"
+cluster_addr = "https://10.0.0.5:8201"
+
listener "tcp" {
+ address = "[::]:8200"
+ cluster_address = "[::]:8201"
+}
+
listener "tcp" {
+ address = "[2001:1c04:90d:1c00:a00:27ff:fefa:58ec]:8200"
+ cluster_address = "[2001:1c04:90d:1c00:a00:27ff:fefa:58ec]:8201"
+}
+
+# Advertise the non-loopback interface
+api_addr = "https://[2001:1c04:90d:1c00:a00:27ff:fefa:58ec]:8200"
+cluster_addr = "https://[2001:1c04:90d:1c00:a00:27ff:fefa:58ec]:8201"
+
Vault Audit은 -path
를 달리하여 여러 Audit 메커니즘을 중복해서 구성 가능
$ vault audit enable file file_path=/var/log/vault/vault_audit.log
+$ vault audit enable -path=file2 file file_path=/var/log/vault/vault_audit2.log
+
$ vault audit enable syslog tag="vault" facility="AUTH"
+
$ vault audit enable socket address=127.0.0.1:9090 socket_type=tcp
+
sudo apt install -y netcat
+nc -l 9090
+
볼트 개발 모드 서버를 시작하는 기초적인 커맨드와 실행 후 안내 메시지는 다음과 같다.
$ vault server -dev
+
+==> Vault server configuration:
+
+ Api Address: http://127.0.0.1:8200
+ Cgo: disabled
+ Cluster Address: https://127.0.0.1:8201
+ Environment Variables: HOME, ITERM_PROFILE, ...
+ Go Version: go1.19.4
+ Listener 1: tcp (addr: "127.0.0.1:8200", cluster address: "127.0.0.1:8201", max_request_duration: "1m30s", max_request_size: "33554432", tls: "disabled")
+ Log Level: info
+ Mlock: supported: false, enabled: false
+ Recovery Mode: false
+ Storage: inmem
+ Version: Vault v1.12.3, built 2023-02-02T09:07:27Z
+ Version Sha: 209b3dd99fe8ca320340d08c70cff5f620261f9b
+
+==> Vault server started! Log data will stream in below:
+
+...
+
개발 모드에서 제공하는 옵션을 확인하기 위해 vault server -h
커맨드를 실행하면 하단에서 Dev Opstions
를 확인할 수 있다.
$ vault server -h
+
+...
+Dev Options:
+
+ -dev
+ Enable development mode. In this mode, Vault runs in-memory and starts
+ unsealed. As the name implies, do not run "dev" mode in production. The
+ default is false.
+
+ -dev-listen-address=<string>
+ Address to bind to in "dev" mode. The default is 127.0.0.1:8200. This
+ can also be specified via the VAULT_DEV_LISTEN_ADDRESS environment
+ variable.
+
+ -dev-no-store-token
+ Do not persist the dev root token to the token helper (usually the local
+ filesystem) for use in future requests. The token will only be displayed
+ in the command output. The default is false.
+
+ -dev-root-token-id=<string>
+ Initial root token. This only applies when running in "dev" mode.
+ This can also be specified via the VAULT_DEV_ROOT_TOKEN_ID environment
+ variable.
+
+ -dev-tls
+ Enable TLS development mode. In this mode, Vault runs in-memory and
+ starts unsealed, with a generated TLS CA, certificate and key. As the
+ name implies, do not run "dev-tls" mode in production. The default is
+ false.
+
+ -dev-tls-cert-dir=<string>
+ Directory where generated TLS files are created if `-dev-tls` is
+ specified. If left unset, files are generated in a temporary directory.
+
개발 모드로 실행하는 -dev
옵션 외 다른 옵션에 대한 설명은 다음과 같다.
-dev-listen-address
: 기본은 127.0.0.1:8200
이며, 변경을 원하는 경우 여기 입력한다. VAULT_DEV_LISTEN_ADDRESS
환경변수로도 대체 가능하다.-dev-no-store-token
: 개발 모드시 사용자 디렉토리에 저장되는 .vault-token
을 생성하지 않는 경우 true
로 설정한다.-dev-root-token-id
: Root Token
은 임의의 값으로 생성되는데, 개발 모드 한정으로 사용자가 지정한 문자열로 정의할 수 있다.-dev-tls
: 개발 모드를 위한 임시 TLS 인증서를 적용하고 Api Address
를 HTTPS
로 설정한다. 실행 후 출력에 VAULT_CACERT
로 임의 생성된 인증서 위치와 설정을 출력한다.-dev-tls-cert-dir
: -dev-tls
가 적용된 경우 사용자가 보유한 인증서를 적용하려는 경우 대상 디렉토리를 지정한다.다음과 같이 옵션을 적용한 개발 모드 볼트를 실행하면, Api Address
가 지정된 주소로 설정되고, .vault-token
은 생성되지 않고, Root Token
이 root
로 설정되고, 생성된 인증서 위치와 커맨드 실행에 필요한 VAULT_CACERT
가 출력되는 것을 확인할 수 있다.
$ vault server -dev -dev-listen-address=0.0.0.0:8200 -dev-no-store-token=true -dev-root-token-id=root -dev-tls
+
+==> Vault server configuration:
+
+ Api Address: https://0.0.0.0:8200
+ ...
+ Listener 1: tcp (addr: "0.0.0.0:8200", cluster address: "0.0.0.0:8201", max_request_duration: "1m30s", max_request_size: "33554432", tls: "enabled")
+
+==> Vault server started! Log data will stream in below:
+
+...
+
+You may need to set the following environment variables:
+
+ $ export VAULT_ADDR='https://0.0.0.0:8200'
+ $ export VAULT_CACERT='/var/folders/5r/8y6t82xd1h183tq1l_whv8yw0000gn/T/vault-tls3194599057/vault-ca.pem'
+
+The unseal key and root token are displayed below in case you want to
+seal/unseal the Vault or re-authenticate.
+
+Unseal Key: DfFexcExFfbN3jRu7cRuAvMeLMpP9J9yNTgzvvcO8Gw=
+Root Token: root
+
+Development mode should NOT be used in production installations!
+
참고:
현재 Vault 비밀 오퍼레이터는 공개 베타 버전입니다. *here*에서 GitHub 이슈를 개설하여 피드백을 제공해 주세요.
Vault Secrets Operator(이하, VSO)를 사용하면 파드가 쿠버네티스 시크릿에서 기본적으로 볼트 시크릿을 사용할 수 있다.
VSO는 지원되는 Custom Resource Definitions (CRD) 집합의 변경 사항을 감시하여 작동한다. 각 CRD는 오퍼레이터가 Vault Secrets을 Kubernetes Secret에 동기화할 수 있도록 하는 데 필요한 사양(specification)을 제공한다.
오퍼레이터는 소스(source) 볼트 시크릿 데이터를 대상(destination) 쿠버네티스 시크릿에 직접 쓰며, 소스에 대한 변경 사항이 수명 기간 동안 대상에 복제되도록 한다. 이러한 방식으로 애플리케이션은 대상 시크릿에 대한 접근 권한만 있으면 그 안에 포함된 시크릿 데이터를 사용할 수 있다.
VSO가 지원하는 기능은 다음과 같다:
Pod
의 ServiceAccount
를 사용한 인증.Deployment
, ReplicaSet
, StatefulSet
쿠버네티스 리소스 유형에 대한 시크릿 로테이션.Helm
, Kustomize
현재 지원되는 Kubernetes minor releases는 다음과 같다. 최신 버전은 각 쿠버네티스 버전에 대해 테스트되며, 다른 버전의 Kubernetes에서도 작동할 수 있지만 지원되지는 않는다.
참고:
현재, 오퍼레이터는 쿠버네티스 인증 방법만 지원한다. 시간이 지남에 따라 더 많은 Vault 인증 방식에 대한 지원을 추가할 예정이다.
Vault 연결 및 인증 구성은 VaultConnection
및 VaultAuth
CRD에서 제공한다. 이는 모든 비밀 복제 유형 리소스가 참조하는 기본 사용자 지정 리소스로 간주할 수 있다.
VaultConnection
커스텀 리소스오퍼레이터가 단일 Vault 서버 인스턴스에 연결하는 데 필요한 구성을 제공한다.
---
+apiVersion: secrets.hashicorp.com/v1alpha1
+kind: VaultConnection
+metadata:
+ namespace: vso-example
+ name: example
+spec:
+ # 필수적인 구성정보
+ # Vault 서버 주소
+ address: http://vault.vault.svc.cluster.local:8200
+
+ # 선택적인 구성정보
+ # 모든 Vault 요청에 포함될 HTTP headers
+ # headers: []
+
+ # TLS 연결의 SNI 호스트로 사용할 TLS 서버 이름
+ # tlsServerName: ""
+
+ # Vault에 대한 TLS 연결에 대한 TLS verification을 건너뜀
+ # skipTLSVerify: false
+
+ # Kubernetes Secret에 저장된 신뢰할 수 있는 PEM 인코딩된 CA 인증서 체인
+ # caCertSecretRef: ""
+
오퍼레이터가 VaultConnection
사용자 지정 리소스에 지정된 대로 단일 Vault 서버 인스턴스에 인증하는 데 필요한 구성을 제공한다.
---
+apiVersion: secrets.hashicorp.com/v1alpha1
+kind: VaultAuth
+metadata:
+ namespace: vso-example
+ name: example
+spec:
+ # 필수적인 구성정보
+ # 해당 VaultConnection 커스텀 리소스의 VaultConnectionRef
+ # 값을 지정하지 않으면 오퍼레이터는 자체 쿠버네티스 네임스페이스에 구성된
+ # 'default' VaultConnection을 기본값으로 사용
+ vaultConnectionRef: example
+
+ # Vault에 인증할 때 사용하는 방법.
+ method: kubernetes
+ # Auth methods로 인증할 때 사용할 마운트.
+ mount: kubernetes
+ # 쿠버네티스용 인증 구성을 사용하려면, Method를 쿠버네티스로 설정해야 함
+ kubernetes:
+ # Vault에 인증할 때 사용할 역할
+ role: example
+ # Vault에 인증할 때 사용할 서비스어카운트 파드/애플리케이션당 항상 고유한 서비스어카운트를 제공을 권장
+ serviceAccount: default
+
+ # 선택적인 구성정보
+ # 인증 백엔드가 마운트되는 Vault 네임스페이스(Vault Enterprise 전용기능)
+ # namespace: ""
+
+ # Vault에 인증할 때 사용할 매개변수
+ # params: []
+
+ # 모든 Vault 인증 요청에 포함할 HTTP 헤더
+ # headers: []
+
오퍼레이터가 단일 볼트 시크릿을 단일 쿠버네티스 시크릿으로 복제하는 데 필요한 구성을 제공한다. 지원되는 각 CRD는 아래 문서에 설명된 Vault Secret의 Class에 특화되어 있다.
VaultStaticSecret
사용자 지정 리소스오퍼레이터가 단일 볼트 정적 시크릿을 단일 Kubernetes Secret에 동기화하는 데 필요한 구성을 제공한다.
---
+apiVersion: secrets.hashicorp.com/v1alpha1
+kind: VaultStaticSecret
+metadata:
+ namespace: vso-example
+ name: example
+spec:
+ vaultAuthRef: example
+ mount: kvv2
+ type: kv-v2
+ name: secret
+ refreshAfter: 60s
+ destination:
+ create: true
+ name: static-secret1
+
VaultPKISecret
사용자 지정 리소스운영자가 단일 볼트 PKI 시크릿을 단일 쿠버네티스 시크릿에 동기화하는 데 필요한 구성을 제공한다.
---
+apiVersion: secrets.hashicorp.com/v1alpha1
+kind: VaultPKISecret
+metadata:
+ namespace: vso-example
+ name: example
+spec:
+ vaultAuthRef: example
+ mount: pki
+ name: default
+ commonName: example.com
+ format: pem
+ expiryOffset: 1s
+ ttl: 60s
+ namespace: tenant-1
+ destination:
+ create: true
+ name: pki1
+
오퍼레이터가 단일 볼트 동적 시크릿을 단일 쿠버네티스 시크릿에 동기화하는 데 필요한 구성을 제공한다.
---
+apiVersion: secrets.hashicorp.com/v1alpha1
+kind: VaultDynamicSecret
+metadata:
+ namespace: vso-example
+ name: example
+spec:
+ vaultAuthRef: example
+ mount: db
+ role: postgres
+ destination:
+ create: true
+ name: dynamic1
+
참고:
현재 Vault 비밀 오퍼레이터는 공개 베타 버전입니다. *here*에서 GitHub 이슈를 개설하여 피드백을 제공해 주세요.
Vault Secrets Operator Helm chart는 Vault Secrets Operator(이하, VSO)를 설치하고 구성하는 권고 방안이다.
VSO의 새 인스턴스를 설치하려면 먼저 HashiCorp Helm Repo를 추가하고 Chart에 액세스할 수 있는지 확인한다:
$helm repo add hashicorp https://helm.releases.hashicorp.com
+"hashicorp" has been added to your repositories
+
+$ helm search repo hashicorp/vault-secrets-operator --devel
+NAME CHART VERSION APP VERSION DESCRIPTION
+hashicorp/vault-secrets-operator 0.1.0-beta 0.1.0-beta Official HashiCorp Vault Secrets Operator Chart
+
그런다음 Operator를 설치한다:
$ helm install --create-namespace --namespace vault-secrets-operator vault-secrets-operator hashicorp/vault-secrets-operator --version 0.1.0-beta
+
업그레이드는 기존 설치에서 helm upgrade
로 수행할 수 있다. 설치 또는 업그레이드 전에 항상 --dry-run
으로 헬름을 실행하여 변경 사항을 확인한다.
지원되는 모든 헬름 차트 값은 here에서 확인할 수 있다.
📌 참고:
현재 Vault 비밀 오퍼레이터는 공개 베타 버전입니다. *here*에서 GitHub 이슈를 개설하여 피드백을 제공해 주세요.
본 문서는 HashiCorp 공식 GitHub의 Vault Secret Operator 저장소 에서 제공하는 코드를 활용하여 환경구성 및 샘플 애플리케이션 배포/연동에 대한 상세 분석을 제공한다.
실습을 위해 vault-secrets-operator 저장소를 복제한다.
# 저장소 복제
+$ git clone https://github.com/hashicorp/vault-secrets-operator.git
+
+# 작업 디렉토리 이동
+$ cd vault-secrets-operator
+
📌 참고:
실행결과 : vso-demo-1.sh
$ make setup-kind
+
setup-kind
수행 후 생성된 KinD 클러스터 및 파드정보 확인vault-secrets-operator-control-plane
가 단일노드로 배포된 것을 확인할 수 있다.
$ kubectl get nodes -o wide
+NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
+vault-secrets-operator-control-plane Ready control-plane 3m18s v1.25.3 172.18.0.2 <none> Ubuntu 22.04.1 LTS 5.15.49-linuxkit containerd://1.6.9
+
+$ kubectl get pods -A
+NAMESPACE NAME READY STATUS RESTARTS AGE
+kube-system coredns-565d847f94-42vpm 1/1 Running 0 3m5s
+kube-system coredns-565d847f94-6fsv9 1/1 Running 0 3m5s
+kube-system etcd-vault-secrets-operator-control-plane 1/1 Running 0 3m18s
+kube-system kindnet-9j486 1/1 Running 0 3m6s
+kube-system kube-apiserver-vault-secrets-operator-control-plane 1/1 Running 0 3m18s
+kube-system kube-controller-manager-vault-secrets-operator-control-plane 1/1 Running 0 3m18s
+kube-system kube-proxy-tfqc8 1/1 Running 0 3m6s
+kube-system kube-scheduler-vault-secrets-operator-control-plane 1/1 Running 0 3m17s
+local-path-storage local-path-provisioner-684f458cdd-2dzfn 1/1 Running 0 3m5s
+
📌 참고
실행결과 : vso-demo-2.sh
앞서 생성된 KinD 클러스터 내부에 Vault 클러스터를 배포한다. 이때, 필요한 사전 환경을 Terraform 코드를 통해 자동으로 구성한다.
make setup-integration-test
+
# Pod 확인
+$ kubectl get pods -n vault
+NAME READY STATUS RESTARTS AGE
+vault-0 1/1 Running 0 73s
+
+# vault 상태확인
+$ kubectl exec -n vault -it vault-0 -- vault status
+Key Value
+--- -----
+Seal Type shamir
+Initialized true
+Sealed false
+Total Shares 1
+Threshold 1
+Version 1.13.2
+Build Date 2023-04-25T13:02:50Z
+Storage Type inmem
+Cluster Name vault-cluster-199af322
+Cluster ID 23b647d5-f067-ba94-b359-2fca26af9ff9
+HA Enabled false
+
Terraform의 kubernetes
, helm
프로바이더를 사용하여 다음과 같은 리소스를 자동으로 배포한다.
📌 참고 :
원본코드 : main.tf
📌 참고:
원본소스 : setup.sh
실행결과 : vso-demo-3.sh
setup.sh
스크립트를 실행하여 다음 3가지 시크릿 엔진에 대한 실습 환경을 구성한다.
$ ./config/samples/setup.sh
+
KV 시크릿엔진 Version 1, Version2를 활성화 하고 샘플 데이터를 주입한다.
vault secrets disable kvv2/
+vault secrets enable -path=kvv2 kv-v2
+vault kv put kvv2/secret username="db-readonly-username" password="db-secret-password"
+
+vault secrets disable kvv1/
+vault secrets enable -path=kvv1 -version=1 kv
+vault kv put kvv1/secret username="v1-user" password="v1-password"
+
PKI 시크릿 엔진을 활성화하고 다음 설정을 진행한다.
# PKI Secret 엔진 활성화
+vault secrets disable pki
+vault secrets enable pki
+
+# PKI 인증서 생성
+vault write pki/root/generate/internal \
+ common_name=example.com \
+ ttl=768h
+
+# 설정
+vault write pki/config/urls \
+ issuing_certificates="http://127.0.0.1:8200/v1/pki/ca" \
+ crl_distribution_points="http://127.0.0.1:8200/v1/pki/crl"
+
+# 역할구성
+vault write pki/roles/default \
+ allowed_domains=example.com \
+ allowed_domains=localhost \
+ allow_subdomains=true \
+ max_ttl=72h
+
각 시크릿 엔진에 대한 ACL Policy를 정의하기 위해 다음 hcl
을 작성하고 적용한다.
# policy.hcl 작성
+cat <<EOT > /tmp/policy.hcl
+path "kvv2/*" {
+ capabilities = ["read"]
+}
+path "kvv1/*" {
+ capabilities = ["read"]
+}
+path "pki/*" {
+ capabilities = ["read", "create", "update"]
+}
+EOT
+
+# demo 정책 생성
+vault policy write demo /tmp/policy.hcl
+
vault policy write
명령으로 정책을 생성하고 확인한다.
Vault와 연동을 위해 kubernetes 인증방식을 설정한다.
참고:
Beta 버전에서는 Kubernetes 인증 방식만 제공
# Kubernetes 인증방식 활성화
+vault auth disable kubernetes
+vault auth enable kubernetes
+
+vault write auth/kubernetes/config \
+ kubernetes_host=https://kubernetes.default.svc
+
+vault write auth/kubernetes/role/demo \
+ bound_service_account_names=default \
+ bound_service_account_namespaces=tenant-1,tenant-2 \
+ policies=demo \
+ ttl=1h
+
VSO에서는 현재 Kubernetes 인증 방식만을 제공하고 있으므로 Kubernetes 인증 방식을 통해 실습을 진행한다.
kubernetes 인증방식 구성을 위해 Roles, Config를 정의한다.
default
tenant-1,tenant-2
demo
1h
(3600s)https://kubernetes.default.svc
(참고) Entity 확인
K8s 인증방식의 역할(Role)에서 사용할 네임스페이스 확인
kubectl get ns | grep tenant
+tenant-1 Active 5h2m
+tenant-2 Active 5h2m
+
Vault 설정이 완료되었으므로 실제 Kubernetes Cluster에서 Operator를 배포한다.
$ make build docker-build deploy-kind
+
$ kubectl get pods -n vault-secrets-operator-system
+NAME READY STATUS RESTARTS AGE
+vault-secrets-operator-controller-manager-6f8b6b8f49-5lt97 2/2 Running 0 3h59m
+
+$ k get crd -A
+NAME CREATED AT
+vaultauths.secrets.hashicorp.com 2023-05-12T08:37:15Z
+vaultconnections.secrets.hashicorp.com 2023-05-12T08:37:15Z
+vaultdynamicsecrets.secrets.hashicorp.com 2023-05-12T08:37:15Z
+vaultpkisecrets.secrets.hashicorp.com 2023-05-12T08:37:15Z
+vaultstaticsecrets.secrets.hashicorp.com 2023-05-12T08:37:15Z
+
$ kubectl apply -k config/samples
+
+secret/pki1 created
+secret/secret1 created
+secret/secret1 created
+service/tls-app-service created
+ingress.networking.k8s.io/tls-example-ingress created
+vaultauth.secrets.hashicorp.com/vaultauth-sample created
+vaultauth.secrets.hashicorp.com/vaultauth-sample created
+vaultconnection.secrets.hashicorp.com/vaultconnection-sample created
+vaultconnection.secrets.hashicorp.com/vaultconnection-sample created
+vaultdynamicsecret.secrets.hashicorp.com/vaultdynamicsecret-sample created
+vaultpkisecret.secrets.hashicorp.com/vaultpkisecret-sample-tenant-1 created
+vaultpkisecret.secrets.hashicorp.com/vaultpkisecret-tls created
+vaultstaticsecret.secrets.hashicorp.com/vaultstaticsecret-sample-tenant-1 created
+vaultstaticsecret.secrets.hashicorp.com/vaultstaticsecret-sample-tenant-2 created
+pod/app1 created
+pod/tls-app created
+pod/app1 created
+
$ kubectl get secrets -n tenant-1 secret1 -o yaml
+$ kubectl get secrets -n tenant-1 pki1 -o yaml
+$ kubectl get secrets -n tenant-2 secret1 -o yaml
+
설명추가
VaultConnection
커스텀 리소스Vault Operator가 연결할 Vault Cluster 정보를 구성한다.
.spec.address
: http://vault.vault.svc.cluster.local:8200---
+apiVersion: secrets.hashicorp.com/v1alpha1
+kind: VaultConnection
+metadata:
+ labels:
+ app.kubernetes.io/name: vaultconnection
+ app.kubernetes.io/instance: vaultconnection-sample
+ app.kubernetes.io/part-of: vault-secrets-operator
+ app.kubernetes.io/managed-by: kustomize
+ app.kubernetes.io/created-by: vault-secrets-operator
+ name: vaultconnection-sample
+ namespace: tenant-1
+spec:
+ address: http://vault.vault.svc.cluster.local:8200
+---
+apiVersion: secrets.hashicorp.com/v1alpha1
+kind: VaultConnection
+metadata:
+ labels:
+ app.kubernetes.io/name: vaultconnection
+ app.kubernetes.io/instance: vaultconnection-sample
+ app.kubernetes.io/part-of: vault-secrets-operator
+ app.kubernetes.io/managed-by: kustomize
+ app.kubernetes.io/created-by: vault-secrets-operator
+ name: vaultconnection-sample
+ namespace: tenant-2
+spec:
+ address: http://vault.vault.svc.cluster.local:8200
+
VaultAuth
커스텀 리소스사전에 정의된 VaultConnection
을 통해 Operator가 Vault Server와 연결할 때, 어떤 인증방식을 사용할지 구성한다.
참고 : Beta 버전에서는 K8s 인증방식만 제공
.spec.vaultConnectionRef
.spec.method
.spec.mount
.spec.kubernetes.role
.spec.kubernetes.serviceAccount
---
+apiVersion: secrets.hashicorp.com/v1alpha1
+kind: VaultAuth
+metadata:
+ labels:
+ app.kubernetes.io/name: vaultauth
+ app.kubernetes.io/instance: vaultauth-sample
+ app.kubernetes.io/part-of: vault-secrets-operator
+ app.kubernetes.io/managed-by: kustomize
+ app.kubernetes.io/created-by: vault-secrets-operator
+ name: vaultauth-sample
+ namespace: tenant-1
+spec:
+ vaultConnectionRef: vaultconnection-sample
+ method: kubernetes
+ mount: kubernetes
+ kubernetes:
+ role: demo
+ serviceAccount: default
+---
+apiVersion: secrets.hashicorp.com/v1alpha1
+kind: VaultAuth
+metadata:
+ labels:
+ app.kubernetes.io/name: vaultauth
+ app.kubernetes.io/instance: vaultauth-sample
+ app.kubernetes.io/part-of: vault-secrets-operator
+ app.kubernetes.io/managed-by: kustomize
+ app.kubernetes.io/created-by: vault-secrets-operator
+ name: vaultauth-sample
+ namespace: tenant-2
+spec:
+ vaultConnectionRef: vaultconnection-sample
+ method: kubernetes
+ mount: kubernetes
+ kubernetes:
+ role: demo
+ serviceAccount: default
+
VSO에서 제공하는 3가지 CRD를 사용하여 Kubernetes 오브젝트와 연동하여 사용하는 방법을 알아본다.
VaultPKISecret
: Pod + PKI Secret다음은 PKI 인증서를 생성하고 Nginx 웹 서버에 적용하는 실습 예제이다. Nginx 파드를 생성할 때 secret 타입의 볼륨을 마운트한다.
VaultPKISecret
---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: pki1
+ namespace: tenant-1
+type: Opaque
+---
+apiVersion: secrets.hashicorp.com/v1alpha1
+kind: VaultPKISecret
+metadata:
+ namespace: tenant-1
+ name: vaultpkisecret-sample-tenant-1
+spec:
+ vaultAuthRef: vaultauth-sample
+ namespace: tenant-1
+ mount: pki
+ name: default
+ destination:
+ name: pki1
+ commonName: consul.example.com
+ format: pem
+ revoke: true
+ clear: true
+ expiryOffset: 5s
+ ttl: 15s
+
---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: app1
+ namespace: tenant-1
+spec:
+ containers:
+ - name: nginx
+ image: nginx
+ volumeMounts:
+ - name: secrets
+ mountPath: "/etc/secrets"
+ readOnly: true
+ volumes:
+ - name: secrets
+ secret:
+ # created in Terraform
+ secretName: pki1
+ optional: false # default setting; "mysecret" must exist
+
실제 PKI 인증서가 정상적으로 생성되는 확인해본다.
/etc/secrets
디렉토에서 파일목록 확인$ ls -lrt /etc/secrets
+
+total 0
+lrwxrwxrwx 1 root root 20 May 14 08:33 serial_number -> ..data/serial_number
+lrwxrwxrwx 1 root root 23 May 14 08:33 private_key_type -> ..data/private_key_type
+lrwxrwxrwx 1 root root 18 May 14 08:33 private_key -> ..data/private_key
+lrwxrwxrwx 1 root root 17 May 14 08:33 issuing_ca -> ..data/issuing_ca
+lrwxrwxrwx 1 root root 17 May 14 08:33 expiration -> ..data/expiration
+lrwxrwxrwx 1 root root 18 May 14 08:33 certificate -> ..data/certificate
+lrwxrwxrwx 1 root root 15 May 14 08:33 ca_chain -> ..data/ca_chain
+lrwxrwxrwx 1 root root 11 May 14 08:33 _raw -> ..data/_raw
+
본 실습에서는 실제 nginx 파드의 구성파일에 PKI 인증서를 적용하는 시나리오가 아닌 단순 파일생성 및 갱신해보았다.
VaultPKISecret
예제2 : Ingress + Pod + PKI Secret이번 실습에서는 앞서 확인한 PKI 인증서를 활용하여 K8s Ingress 오브젝트에 적용하고 주기적으로 교체되는 시나리오를 확인해본다.
참고 : Ingress 실습을 위해서는 Nginx Ingress Controller를 설치 후 진행해야 한다. [참고]
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
+
+kubectl wait --namespace ingress-nginx \
+ --for=condition=ready pod \
+ --selector=app.kubernetes.io/component=controller \
+ --timeout=90s
+
---
+apiVersion: secrets.hashicorp.com/v1alpha1
+kind: VaultPKISecret
+metadata:
+ name: vaultpkisecret-tls
+ namespace: tenant-1
+spec:
+ vaultAuthRef: vaultauth-sample
+ namespace: tenant-1
+ mount: pki
+ name: default
+ destination:
+ create: true
+ name: pki-tls
+ type: kubernetes.io/tls
+ commonName: localhost
+ format: pem
+ revoke: true
+ clear: true
+ expiryOffset: 15s
+ ttl: 1m
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: tls-app
+ namespace: tenant-1
+ labels:
+ app: tls-app
+spec:
+ containers:
+ - command:
+ - /agnhost
+ - netexec
+ - --http-port
+ - "8080"
+ image: registry.k8s.io/e2e-test-images/agnhost:2.39
+ name: tls-app
+---
+kind: Service
+apiVersion: v1
+metadata:
+ name: tls-app-service
+ namespace: tenant-1
+spec:
+ selector:
+ app: tls-app
+ ports:
+ - port: 443
+ targetPort: 8080
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: tls-example-ingress
+ namespace: tenant-1
+ annotations:
+ nginx.ingress.kubernetes.io/rewrite-target: /$2
+spec:
+ tls:
+ - hosts:
+ - localhost
+ secretName: pki-tls
+ rules:
+ - host: localhost
+ http:
+ paths:
+ - path: /tls-app(/|$)(.*)
+ pathType: Prefix
+ backend:
+ service:
+ name: tls-app-service
+ port:
+ number: 443
+
$ curl -k https://localhost:38443/tls-app/hostname
+tls-app%
+$ curl -kvI https://localhost:38443/tls-app/hostname
+* Trying 127.0.0.1:38443...
+* Connected to localhost (127.0.0.1) port 38443 (#0)
+# 중략
+* Server certificate:
+* subject: CN=localhost
+* start date: May 14 08:04:00 2023 GMT
+* expire date: May 14 08:05:30 2023 GMT
+* issuer: CN=example.com
+
kubectl logs -f -n ingress-nginx -l app.kubernetes.io/instance=ingress-nginx
+W0514 07:51:58.673604 1 client_config.go:615] Neither --kubeconfig nor --master was specified. Using the inClusterConfig. This might not work.
+{"level":"info","msg":"patching webhook configurations 'ingress-nginx-admission' mutating=false, validating=true, failurePolicy=Fail","source":"k8s/k8s.go:118","time":"2023-05-14T07:51:58Z"}
+{"level":"info","msg":"Patched hook(s)","source":"k8s/k8s.go:138","time":"2023-05-14T07:51:58Z"}
+I0514 08:19:30.110926 9 store.go:619] "secret was updated and it is used in ingress annotations. Parsing" secret="tenant-1/pki-tls"
+I0514 08:19:30.113988 9 backend_ssl.go:59] "Updating secret in local store" name="tenant-1/pki-tls"
+W0514 08:19:30.114178 9 controller.go:1406] SSL certificate for server "localhost" is about to expire (2023-05-14 08:20:30 +0000 UTC)
+I0514 08:20:15.208102 9 store.go:619] "secret was updated and it is used in ingress annotations. Parsing" secret="tenant-1/pki-tls"
+I0514 08:20:15.208539 9 backend_ssl.go:59] "Updating secret in local store" name="tenant-1/pki-tls"
+W0514 08:20:15.208801 9 controller.go:1406] SSL certificate for server "localhost" is about to expire (2023-05-14 08:21:15 +0000 UTC)
+W0514 08:20:18.543113 9 controller.go:1406] SSL certificate for server "localhost" is about to expire (2023-05-14 08:21:15 +0000 UTC)
+I0514 08:21:00.107794 9 store.go:619] "secret was updated and it is used in ingress annotations. Parsing" secret="tenant-1/pki-tls"
+I0514 08:21:00.108127 9 backend_ssl.go:59] "Updating secret in local store" name="tenant-1/pki-tls"
+W0514 08:21:00.108295 9 controller.go:1406] SSL certificate for server "localhost" is about to expire (2023-05-14 08:22:00 +0000 UTC)
+W0514 07:51:58.418022 1 client_config.go:615] Neither --kubeconfig nor --master was specified. Using the inClusterConfig. This might not work.
+{"err":"secrets \"ingress-nginx-admission\" not found","level":"info","msg":"no secret found","source":"k8s/k8s.go:229","time":"2023-05-14T07:51:58Z"}
+{"level":"info","msg":"creating new secret","source":"cmd/create.go:28","time":"2023-05-14T07:51:58Z"}
+
VaultStaticSecret
예제 :---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: secret1
+ namespace: tenant-1
+type: Opaque
+---
+apiVersion: secrets.hashicorp.com/v1alpha1
+kind: VaultStaticSecret
+metadata:
+ namespace: tenant-1
+ name: vaultstaticsecret-sample-tenant-1
+spec:
+ # namespace: cluster1/tenant-1
+ vaultAuthRef: vaultauth-sample
+ mount: kvv2
+ type: kv-v2
+ name: secret
+ refreshAfter: 5s
+ destination:
+ name: secret1
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: secret1
+ namespace: tenant-2
+type: Opaque
+---
+apiVersion: secrets.hashicorp.com/v1alpha1
+kind: VaultStaticSecret
+metadata:
+ namespace: tenant-2
+ name: vaultstaticsecret-sample-tenant-2
+spec:
+ # namespace: cluster1/tenant-2
+ vaultAuthRef: vaultauth-sample
+ mount: kvv1
+ type: kv-v1
+ name: secret
+ refreshAfter: 5s
+ destination:
+ name: secret1
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: app1
+ namespace: tenant-1
+spec:
+ containers:
+ - name: nginx
+ image: nginx
+ volumeMounts:
+ - name: secrets
+ mountPath: "/etc/secrets"
+ readOnly: true
+ volumes:
+ - name: secrets
+ secret:
+ secretName: secret1
+ optional: false # default setting; "mysecret" must exist
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: app1
+ namespace: tenant-2
+spec:
+ containers:
+ - name: nginx
+ image: nginx
+ volumeMounts:
+ - name: secrets
+ mountPath: "/etc/secrets"
+ readOnly: true
+ volumes:
+ - name: secrets
+ secret:
+ secretName: secret1
+ optional: false # default setting; "mysecret" must exist
+
VaultDynamicSecret
🔥 업데이트 예정
apiVersion: secrets.hashicorp.com/v1alpha1
+kind: VaultDynamicSecret
+metadata:
+ labels:
+ app.kubernetes.io/name: vaultdynamicsecret
+ app.kubernetes.io/instance: vaultdynamicsecret-sample
+ app.kubernetes.io/part-of: vault-secrets-operator
+ app.kubernetes.io/managed-by: kustomize
+ app.kubernetes.io/created-by: vault-secrets-operator
+ name: vaultdynamicsecret-sample
+spec:
+ # TODO(user): Add fields here
+
샘플 삭제:
# K8s 리소스 삭제
+$ kubectl delete -k config/samples
+
+# kind 클러스터 삭제
+$ kind delete clusters vault-secrets-operator
+
볼트 서버를 시작하는 기초적인 커맨드와 실행 후 안내 메시지는 다음과 같다.
$ vault server -dev
+
+==> Vault server configuration:
+
+ Api Address: http://127.0.0.1:8200
+ Cgo: disabled
+ Cluster Address: https://127.0.0.1:8201
+ Environment Variables: HOME, ITERM_PROFILE, ...
+ Go Version: go1.19.4
+ Listener 1: tcp (addr: "127.0.0.1:8200", cluster address: "127.0.0.1:8201", max_request_duration: "1m30s", max_request_size: "33554432", tls: "disabled")
+ Log Level: info
+ Mlock: supported: false, enabled: false
+ Recovery Mode: false
+ Storage: inmem
+ Version: Vault v1.12.3, built 2023-02-02T09:07:27Z
+ Version Sha: 209b3dd99fe8ca320340d08c70cff5f620261f9b
+
+==> Vault server started! Log data will stream in below:
+
+...
+
기동 후 서버가 실행된 주요 설정 정보의 설명인 Vault server configuration
의 내용의 설명은 다음과 같다.
Api Address: http://127.0.0.1:8200
: 볼트와의 인터페이스를 위한 API 주소로, RestAPI, CLI, UI 모두 해당 주소와 통신한다.
Cgo: disabled
: 볼트는 golang으로 이루어져 있는데, go 컴파일 시 CGO_ENABLED
가 활성화되어있다면 dynamic library로 빌드된다. 이 경우 실행 환경에 컴파일 시 사용되는 링크 모듈이 존재하지 않으면 에러가 발생하므로, 실행된 볼트 바이너리는 빌드시 static library로 빌드되었다는 의미다.
Cluster Address: https://127.0.0.1:8201
: 볼트 서버를 고가용성(HA)을 위해 다수의 서버로 구성한 경우 서버 간에 통신하는데 사용되는 주소로, TLS1.2로 상호간 인증한다.
Environment Variables: HOME, ITERM_PROFILE, ...
: 볼트 서버가 실행된 환경의 환경변수 값의 나열로, 볼트 실행에 영향을 주는 환경변수가 있는지 확인할 수 있다.
Go Version: go1.19.4
: 볼트 서버가 빌드 된 golang 버전이다.
Listener 1: tcp (addr: "127.0.0.1:8200", cluster address: "127.0.0.1:8201", max_request_duration: "1m30s", max_request_size: "33554432", tls: "disabled")
: 볼트의 리스너 정보로, 이 정보를 기반으로 Api Address
, Cluster Address
가 설정 된다. 숫자가 붙어있다는 의미는 리스터를 여러개 설정 할 수 있다는 의미다.
Log Level: info
: 볼트에서 기록하는 로그 수준이다.
Mlock: supported: false, enabled: false
: 볼트의 기본값은 디스크로의 스와핑을 비활성화하기 위해 메모리상의 가상 주소 공간을 RAM에서 잠근다. 이를 위해서는 mlock()
을 지원하는 시스템, 루트 권한, 설정의 활성화가 필요하다.
Recovery Mode: false
: 볼트 시작시 복구모드로 실행되었는지의 여부이다.
Storage: inmem
: 볼트 저장소 타입이다.
Version: Vault v1.12.3, built 2023-02-02T09:07:27Z
: 볼트 바이너리 버정 정보를 나타낸다.
Version Sha: 209b3dd99fe8ca320340d08c70cff5f620261f9b
: 각 볼트 버전과 관련된 고유 식별자입니다. 실행 중인 코드의 버전을 식별하는 데 사용되며, 볼트 오픈소스의 GitHub Repository의 commit id와 같다.
https://learn.hashicorp.com/tutorials/vault/reference-architecture#deployment-system-requirements
Vault의 Backend-Storage 사용 여부에 따라 구성에 차이가 발생
Size | CPU | Memory | Disk | Typical Cloud Instance Types |
---|---|---|---|---|
최소 | 2 core | 4-8 GB RAM | 50 GB | 2 vCPU, 8 GB Mem, 10Gbps Network |
권장 | 4-8 core | 16-32 GB RAM | 200 GB | 4 vCPU, 16 GB Mem, 10Gbps Network |
Size | CPU | Memory | Disk | Typical Cloud Instance Types |
---|---|---|---|---|
최소 | 2 core | 4-8 GB RAM | 20 GB | 2 vCPU, 8 GB Mem, 10Gbps Network |
권장 | 4-8 core | 16-32 GB RAM | 100 GB | 4 vCPU, 16 GB Mem, 10Gbps Network |
Size | CPU | Memory | Disk | Typical Cloud Instance Types |
---|---|---|---|---|
Small | 2 core | 4-8 GB RAM | 25 GB | 2 vCPU, 8 GB Mem, 10Gbps Network |
Large | 4-8 core | 16-32 GB RAM | 100 GB | 4 vCPU, 16 GB Mem, 10Gbps Network |
다음은 수동으로 Token을 생성하는 방법으로 Token을 생성할 수 있는 권한의 사용자가 CLI를 사용하여 default
Policy를 갖는 Token을 생성하는 경우의 예이다.
$ vault token create -policy=default
+
+Key Value
+--- -----
+token hvs.CAESIO7WUHJ15SkEOgtqzcVuF8pTZdBQmI
+token_accessor yK2enofb1NExLrLFqg136mw5
+token_duration 768h
+token_renewable true
+token_policies ["default"]
+identity_policies []
+policies ["default"]
+
간단한 응답과 달리 API로 요청하면 더 상세한 응답 결과를 확인할 수 있다.
# payload.json
+{
+ "policies": ["default"],
+ "meta": {
+ "user": "armon"
+ },
+ "ttl": "1h",
+ "renewable": true
+}
+
+# API 요청
+curl \
+ --header "X-Vault-Token: root" \
+ --request POST \
+ --data @payload.json \
+ http://127.0.0.1:8200/v1/auth/token/create | jq .
+
+# API 응답
+{
+ "request_id": "a0a87aea-3627-a2a6-ab3c-8c08285fdc7d",
+ "lease_id": "",
+ "renewable": false,
+ "lease_duration": 0,
+ "data": null,
+ "wrap_info": null,
+ "warnings": [
+ "Endpoint ignored these unrecognized parameters: [meta]"
+ ],
+ "auth": {
+ "client_token": "hvs.CAESIO-LeVOy1mOLSz1f-yDC22cFqOXQ2u5a3hmLVxeZ1V07Gh4KHGh2cy5mTmJZbERwZ0xLeldqeFgwYWRyc3Z4a0g",
+ "accessor": "ArOmYq9MuDyo1wZkLisad6Ml",
+ "policies": [
+ "default"
+ ],
+ "token_policies": [
+ "default"
+ ],
+ "metadata": {
+ "user": "armon"
+ },
+ "lease_duration": 3600,
+ "renewable": true,
+ "entity_id": "",
+ "token_type": "service",
+ "orphan": false,
+ "mfa_requirement": null,
+ "num_uses": 0
+ }
+}
+
다음은 userpass 인증방식을 CLI를 사용하여 default
Policy를 갖는 Token을 발급받는 경우의 예이다. 이경우 발급된 Token은 시스템 홈디렉터리의 .vault-token
파일에 Token이 저장된다.
$ vault login -method=userpass username=admin
+Password (will be hidden): ********
+
+Success! You are now authenticated. The token information displayed below
+is already stored in the token helper. You do NOT need to run "vault login"
+again. Future Vault requests will automatically use this token.
+
+Key Value
+--- -----
+token hvs.CAESIFyNxoV1I-_nFeBh9LBxDB9oGNghX
+token_accessor 80nJPKtpaPbMUyQ715VkRGig
+token_duration 768h
+token_renewable true
+token_policies ["default"]
+identity_policies []
+policies ["default"]
+token_meta_username admin
+
CLI 출력을 기준으로 출력된 Key
의 설명은 다음과 같다.
CLI 결과 | API 결과 | 설명 |
---|---|---|
token | client_token | 생성된 Token 문자열 값 |
token_accessor | accessor | Token과 쌍으로 생성된 참조 값 |
token_duration | lease_duration | 생성된 Token의 사용 기간 |
token_renewable | renewable | Renewal 가능 여부 |
token_policies | token_policies | 생성시 부여된 Policy |
identity_policies | (entity_id로 조회 필요) | Token이 속한 Identity(Entity)에 부여된 Policy 목록 |
polices | policies | Token에 부여된 전체 Policy 목록 |
token_meta_[key] | metadata | metatada 출력 |
Token이 생성되면 종류에 따라 붙는 Prefix를 보고 유형을 유추할 수 있다.
Token 유형 | <1.9 | >=1.10 |
---|---|---|
Service Token | s. | hvs. |
Batch Token | b. | hvb. |
Recovery Token | r. | hvr. |
Token의 여러 속성 중 종속성의 이해가 필요하다. 종속성으로 인해 상위 Token이 취소되거나 만료되면 하위 Token도 함께 취소된다. 동작을 확인하기 위해 독립된 Orphan Token을 생성하고 하위 Token을 생성 한 뒤 상위 Token을 취소했을 때 현상을 확인하여 종속성의 결과를 확인 가능하다.
Root Token이 VAULT_TOKEN
환경변수에 정의된 상태에서 모든 권한을 갖는 Policy를 생성한다.
# super-user.hcl
+path "*" {
+ capabilities = ["create", "read", "update", "delete", "list", "sudo"]
+}
+
+# Policy 적용
+$ vault policy write super-user super-user.hcl
+
독립된 Token을 생성한다. 이 Token의 TTL은 60초로 짧게 부여한다.
$ vault token create -policy=super-user -ttl=60s -orphan
+
새로 생성된 Token을 VAULT_TOKEN
환경변수로 지정한 상태에서 새로운 하위 Token을 생성한다. 이 Token의 TTL은 768시간으로 길게 부여한다.
$ vault token create -policy=default -ttl=768h
+
-orphan
옵션이 붙지 않은 Token은 상위 Token에 종속되므로 768시간의 TTL을 갖고 있지만 상위 Token이 60초가 지나 만료되면 해당 하위 토큰도 함께 만료됨을 확인할 수 있다.
$ vault token <CHILD_TOKEN>
+Error looking up token: Error making API request.
+
+URL: POST http://127.0.0.1:8200/v1/auth/token/lookup
+Code: 403. Errors:
+
+* bad token
+
Token 종속성으로인한 하위 Token의 의도하지 않은 취소를 피하려면 Orphan(고아) Token으로의 지정이 필요하다. Token을 생성한 Parent(부모/상위) Token과 생성된 Child(자식/하위) Token의 관계가 형성되며, Orphan Token의 경우 Parent에서 독립되어 Token Tree의 루트가 된다. Orphan Token을 생성하는 방안은 다음과 같다.
auth/token/create-orphan
엔드포인트auth/token/create-orphan
엔드포인트에 write
권한이 필요하다.
path "auth/token/create-orphan" {
+ capabilities = ["create", "update"]
+}
+
Token 생성 시 해당 엔드포인트로 요청하여 Orphan Token을 생성한다.
$ vault write -force auth/token/create-orphan policies=default
+
auth/token/create
요청 시 no_parent
옵션 사용[방안 1]과 같이 Orphan을 위한 엔드포인트 외에 일반적인 Token 생성 엔드포인트를 요청하는 경우 no_parent
옵션을 true
로 설정하여 요청한다. auth/token/create
엔드포인트에 write
권한 필요하다.
path "auth/token/create" {
+ capabilities = ["create", "update"]
+}
+
Token 생성 시 해당 엔드포인트로 요청하여 Orphan Token을 생성한다.
$ vault write -force auth/token/create policies=default no_parent=true
+
vault write
요청과 함께, Token을 위한 전용 CLI 인 vault token create
를 사용할 수 있다.
$ vault token create -policy="default" -orphan
+
Token을 필요한 시점마다 만드는 것이 아닌 미리 정의된 Token Role을 기반으로 Token을 생성할 때, 해당 Role에 Orphan으로 생성한다는 정의가 된 경우 생성되는 Token은 Orphan Token이 된다. Token Role을 CLI로 생성하는 방법은 다음과 같다. 간단히 허용하는 Policy와 Orphan 여부만 설정하였다.
$ vault write auth/token/roles/my-orphan allowed_policies="default" orphan=true
+
Token Role을 기반으로 Token 생성을 요청하면 정의된 설정에 의해 Token이 생성된다.
$ vault token create -policy=default -role=my-orphan
+
Auth Methods를 통해 인증 후 받게되는 Token은 Orphan Token으로 생성되어 반환된다.
위 [방안 1~4]에서 생성된 Orphan Token은 Token을 조회하면 Orphan 여부를 확인할 수 있다.
$ vault token lookup <VAULT_TOKEN>
+
+Key Value
+--- -----
+...
+orphan true # Orphan 여부
+path auth/userpass/login/admin # Token이 생성된 API 엔드포인트
+policies [default super-user]
+type service
+
Token의 유형은 Service
타입과 Batch
타입으로 나뉘며, 각각은 Orphan 여부에 따라 Token을 생성한 Parent Token과의 종속성을 정의할 수 있다.
기능 | Service Token | Batch Token |
---|---|---|
Root Token 역할 | ✅ | ⛔️ |
Chile Token 생성 | ✅ | ⛔️ |
Renewable(기간 늘림) | ✅ | ⛔️ |
Manually Revocable(수동 취소) | ✅ | ⛔️ |
Periodic 형태 | ✅ | ⛔️ |
Explicit Max TTL 설정 | ✅ | ⛔️ (고정) |
Accessor 여부 | ✅ | ⛔️ |
Cubbyhole 사용 여부 | ✅ | ⛔️ |
Revoke with parent (부모 Token이 취소될 때 같이 취소) | ✅ | Revoke는 아니나 사용 중지 |
Dynamic Secrets lease assignment | 자체 할당 | 부모로 동작 |
Performance Replication Cluster 전체에서 사용 | ⛔️ | ✅ |
Cost | 무거움 : 백엔드 스토리지에 건당 저장 | 가벼움 : 백엔드 스토리지 저장되지 않음 |
Token이 생성되면 Accessor도 함께 생성되는데, 이 Accessor는 Token을 참조하는 값임과 동시에 Token을 직접 알지 못하더라도 Token에 대한 기본 속성이나 Renew(갱신), Revoke(취소) 작업을 수행할 수 있다. Accessor에 대한 작업은 일반적인 read
, write
작업과 더불어 vault token
CLI로도 사용 가능하다.
# Token lookup
+$ vault write auth/token/lookup-accessor accessor=<ACCESSOR>
+$ vault token lookup -accessor <ACCESSOR>
+
+# Token Renew
+$ vault write auth/token/renew-accessor accessor=<ACCESSOR>
+$ vault token renew -accessor <ACCESSOR>
+
+# Token Revoke
+$ vault write auth/token/revoke-accessor accessor=<ACCESSOR>
+$ vault token revoke -accessor <ACCESSOR>
+
이미 발급된 Token을 알수는 없지만 몇몇 동작은 Accessor로 수행이 가능하므로, 이런 Accessor 값을 노출시키는 것은 임의로 Revoke하는 작업이 수반되는 경우 위험할 수 있어 각별히 조심해야 한다.
Root Token을 제외한 모든 Token은 TTL이 부여된다. TTL은 token의 생성 시간 또는 마지막 갱신 시간 중 가장 최근 시간 기준으로 이후의 유효한 기간까지의 시간이다.
vault token renew
명령으로 갱신 가능다음과 같이 TTL과 Explicit Max TTL을 설정하여 Token을 생성해본다.
$ vault token create -policy=default -ttl=60s -explicit-max-ttl=90s
+
생성된 Token의 정보를 확인해보면 적용된 값이 확인된다.
$ vault token lookup <TOKEN>
+Key Value
+--- -----
+creation_ttl 1m
+expire_time 2023-03-04T16:44:40.187494+09:00
+explicit_max_ttl 1m30s
+issue_time 2023-03-04T16:43:40.187497+09:00
+ttl 54s
+
TTL과 관련된 내용을 확인해보면 다음과 같이 해석할 수 있다.
Renew를 수행하면 최초 부여한 60초 만큼을 더하여 TTL을 증가시키는데, Explicit Max TTL이 1분 30초 이므로, 30초가 넘어간 시점에 Renew를 요청하면 추가 60초 만큼을 남은 총량 대비 부여하지 못한다는 메시지를 확인할 수 있다.
$ vault token renew <TOKEN>
+
+WARNING! The following warnings were returned from Vault:
+
+ * TTL of "1m" exceeded the effective max_ttl of "56s"; TTL value is capped
+ accordingly
+
TTL은 특성상 max TTL이 있으므로 영구적으로 사용은 불가능한 속성이다. Token이 만료되는 것이 문제가 될 수 있고, 오랜 기간동안 유지해야 하는 경우 Periodic Token을 사용한다.
vault token renew
로 TTL을 계속 갱신해서 사용Periodic Token을 생성하는 방법은 ttl
대신 period
에 기간을 주어 생성한다.
$ vault token create -policy=default -period=60s
+
생성된 Token의 정보를 확인해보면 period
값이 있는 것이 확인된다.
$ vault token lookup <TOKEN>
+
+Key Value
+--- -----
+explicit_max_ttl 0s
+period 1m
+
explicit_max_ttl
이 0초라는 의미는 무제한으로 풀이할 수 있다. 이후 Renew를 수행하면 지속적으로 TTL이 갱신되는 것을 확인할 수 있다.
$ vault token renew <TOKEN>
+
+Key Value
+--- -----
+token_duration 1m
+
+$ vault token renew <TOKEN>
+
+Key Value
+--- -----
+token_duration 1m
+
+...계속...
+
볼트를 Init하면 최초 발급되는 Token이 Root Token이다. 볼트에서 유일하게 만료되지 않는 Token으로 모든 권한(root
Policy)을 갖고 있다. 일반적으로 Init 후 root
권한을 갖는 일반 인증을 생성한 뒤 파기하는 것을 권장한다.
# Root에 준하는 Policy
+path "*" {
+ capabilities = ["create", "read", "update", "delete", "list", "sudo"]
+}
+
Root Token을 분실했거나 파기 후 필요한 경우 다음 순서에 따라 새로운 Root Token을 발급한다.
https://developer.hashicorp.com/vault/tutorials/operations/generate-root
$ vault operator generate-root -init
+
+A One-Time-Password has been generated for you and is shown in the OTP field.
+You will need this value to decode the resulting root token, so keep it safe.
+Nonce 1a6294ff-1f09-cccf-6434-e49279aec4df
+Started true
+Progress 0/1
+Complete false
+OTP vxa9sXQjPCb91C1rS1yPJYcMw90f
+OTP Length 28
+
생성된 Nonce
와 OTP
가 Unseal 및 인코딩된 Root Token 복호화에 사용된다.
동일한 터미널 상에서 수행했다면 Nonce
값이 자동으로 입력되지만 아닌경우에는 -nonce
속석으로 지정이 필요하다.
$ vault operator generate-root
+or
+$ vault operator generate-root -nonce=1a6294ff-1f09-cccf-6434-e49279aec4df
+
vault operator generate-root
를 수행하면 Unseal 키를 입력해야 한다. Init에서 발생된 Unseal Key 값을 입력하여 인코딩된 Root Token을 발급 받는다.
$ vault operator generate-root
+
+Root generation operation nonce: 1a6294ff-1f09-cccf-6434-e49279aec4df
+Unseal Key (will be hidden):
+
+Nonce 1a6294ff-1f09-cccf-6434-e49279aec4df
+Started true
+Progress 5/5
+Complete true
+Encoded Token Hg4SFzwRZCAoAQFheHRLHmZiFicabzZ7D0pEJw
+
초기화시 발급된 OTP
를 이용하여 인코딩된 Root Token을 복호화 한다.
$ vault operator generate-root \
+ -decode=Hg4SFzwRZCAoAQFheHRLHmZiFicabzZ7D0pEJw \
+ -otp=vxa9sXQjPCb91C1rS1yPJYcMw90f
+
+hvs.OI5JxBcXI7zl5SowP6U6xstA
+
Key Management Secret Engine을 활성화 하기 위해서는
ADP
수준의 라이선스가 필요하다.
Key Management 시크릿 엔진은 KMS(Key Management Service)를 공급하는 대상의 암호화 키의 배포 및 수명 주기 관리를 위한 워크플로를 제공한다. KMS 공급자 고유의 암호화 기능을 기존처럼 사용하면서도, 볼트에서 키를 중앙 집중식으로 제어할 수 있다.
볼트는 KMS의 구성에 사용되는 Key Meterial 원본을 생성하여 보유한다. 관리가능한 KMS에 대해 키 수명주기를 설정 및 관리하면 Key Meterial의 복사본이 대상에 배포된다. 이 방식으로 볼트는 KMS 서비스의 전체 수명 주기 관리 및 키 복구 수단을 제공한다. 지원되는 KMS는 다음과 같다.
Key Management의 구성 및 동작의 순서는 다음과 같다.
Key Management 시크릿 엔진인 keymgmt
를 활성화
키 생성
type
: 키 유형 aes256-gcm96
rsa-2048
, rsa-3072
, rsa-4096
aes256-gcm96
, rsa-2048
, rsa-3072
, rsa-4096
, ecdsa-p256
, ecdsa-p384
지원되는 KMS 서비스 지정 및 공급자 별 인증 정보 등록
awskms
: AWS KMSazurekeyvault
: Azure Key Vaultgcpckms
: GCP Cloud KMSKMS 서비스에 키 생성
purpose
: 목적 enctypt
dectypt
sign
verify
wrap
unwrap
protection
: 키 보호 지정 hsm
software
관리를 위한 키 회전 (선택)
키 버전 활성/비활성 (선택)
KMS 서비스의 키 제거
keymgmt
시크릿 엔진을 활성화하여, 해당 엔드포인트에서 시크릿에 대한 관리를 수행한다. 관리 목적에 따라 별도의 엔드포인트를 path
로 지정한다.
$ vault secrets enable keymgmt
+Success! Enabled the keymgmt secrets engine at: keymgmt/
+
kms:CreateKey
kms:GetParametersForImport
kms:ImportKeyMaterial
kms:EnableKey
kms:DisableKey
kms:ScheduleKeyDeletion
kms:CreateAlias
kms:UpdateAlias
kms:DeleteAlias
kms:ListAliases
kms:TagResource
keymgmt
Secret EngineSecret Engine 에 대한 활성화는 Secret Engine 에 접근하기 위해 사용되는 엔드포인트 정보만 생성되었을 뿐, 연동하고자 하는 대상 AWS 환경에 대한 정보에 대해서는 세부 설정이 필요하다. 이를 위해 Vault 가 대상 AWS 환경에 대해 접근, 자격증명 발급 그리고 생명주기 관리 작업을 수행할 수 있는 권한이 부여된 자격증명 발급이 필요하다.
자격증명 발급 및 생명주기 관리 권한을 위한 정책 생성
AWS IAM 정책 생성: https://docs.aws.amazon.com/ko_kr/IAM/latest/UserGuide/access_policies_create-console.html
https://console.aws.amazon.com/iam/에서 IAM 콘솔에 접속.
왼쪽의 탐색 창에서 정책(Policies) 을 선택.
정책 생성(Create policy) 을 선택.
JSON 탭을 선택.
JSON 정책 문서 입력
ACCOUNT-ID-WITHOUT-HYPHENS
는 AWS 콘솔 우상단에서 확인 가능한 숫자 12자리로 구성된 계정 고유 ID 정보 입력{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "kms:CreateKey",
+ "kms:GetParametersForImport",
+ "kms:ImportKeyMaterial",
+ "kms:EnableKey",
+ "kms:DisableKey",
+ "kms:ScheduleKeyDeletion",
+ "kms:CreateAlias",
+ "kms:UpdateAlias",
+ "kms:DeleteAlias",
+ "kms:ListAliases",
+ "kms:TagResource"
+ ],
+ "Resource": ["*"]
+ }
+ ]
+}
+
보안 경고, 오류 또는 일반 경고를 해결한 다음 정책 검토(Review policy) 선택.
정책 이름 정의 후 정책 생성(Create Policy) 선택 하여 생성 완료
자격증명 발급
AWS 계정 및 액세스 키 : https://docs.aws.amazon.com/ko_kr/powershell/latest/userguide/pstools-appendix-sign-up.html
AKIAIOSFODNN7EXAMPLE
wJalrXUt******************XAMPLEKEY
AWS KMS에서 호환되는 암호화 키를 생성한다.
vault secrets enable keymgmt
+
+$ vault write -f keymgmt/key/aes256-gcm96 type="aes256-gcm96"
+Success! Data written to: keymgmt/key/aes256-gcm96
+
생성된 암호화 키의 정보를 확인한다.
$ vault read keymgmt/key/aes256-gcm96
+Key Value
+--- -----
+deletion_allowed false
+latest_version 1
+min_enabled_version 1
+name aes256-gcm96
+type aes256-gcm96
+versions map[1:map[creation_time:2023-07-05T12:04:48.099141545Z]]
+
AWS KMS 공급자 리소스를 구성한다.
$ vault write keymgmt/kms/aws-kms-anea2 \
+ provider="awskms" \
+ key_collection="ap-northeast-2" \
+ credentials=access_key="ASIADJO3WTX6WPLJM42V" \
+ credentials=secret_key="bCiYmNroLxLmPNQ47VIvjlm8mQu5oktZcQdq195w"
+
+Success! Data written to: keymgmt/kms/aws-kms
+
AWS KMS 공급자 리소스를 구성하는 매개변수의 설명은 다음과 같다.
매개변수 | 설명 |
---|---|
provider | AWS KMS를 구성하는 경우 awskms 를 사용 |
key_collection | AWS 리전을 지정 |
credentials=access_key | AWS Access Key |
credentials=secret_key | AWS Secret Key |
credentials=session_token | AWS Session Token |
credentials=endpoint | AWS KMS API endpoint |
AWS KMS 공급자 리소스에 암호화 키를 배포
vault write keymgmt/kms/:name/key/:key_name
+$ vault write keymgmt/kms/aws-kms-anea2/key/aes256-gcm96 \
+ purpose="encrypt,decrypt" \
+ protection="hsm"
+
+Success! Data written to: keymgmt/kms/aws-kms-anea2/key/aes256-gcm96
+
encrypt
와 decrypt
를 함께 사용한다.hsm
만 사용 가능하다.등록이 완료되면 AWS Console의 Key Management Service(KMS)
에서 고객 관리형 키
로 등록된다.
생성된 키정보를 확인하여 현재 버전에 명시된 ID가 AWS 상의 KMS의 키 ID
와 같은지 확인한다.
$ vault read keymgmt/kms/aws-kms-anea2/key/aes256-gcm96
+Key Value
+--- -----
+distribution_time 2023-07-05T12:14:52.173163449Z
+name aes256-gcm96-1688559292
+protection hsm
+purpose decrypt,encrypt
+versions map[1:503e44bf-7629-47c3-8c22-a5337b3aab3a]
+
AWS KMS사용자의 경우 vault로의 API 요청으로 키ID를 확인할 수 있다.
curl -H "X-Vault-Token: token" -X GET http://<vault_hostname>:<port>/v1/keymgmt/kms/aws-kms-anea2/key/aes256-gcm96
+{
+ "request_id": "e8147c9e-a3fd-71b6-075e-8f2d67393127",
+ "lease_id": "",
+ "lease_duration": 0,
+ "renewable": false,
+ "data": {
+ "distribution_time": "2023-07-05T12:14:52.173163449Z",
+ "name": "aes256-gcm96-1688602383",
+ "protection": "hsm",
+ "purpose": "decrypt,encrypt",
+ "versions": {
+ "1": "503e44bf-7629-47c3-8c22-a5337b3aab3a"
+ }
+ },
+ "warnings": null
+}
+
AWS KMS에 적용된 키를 순환시킨다.
$ vault write -f keymgmt/key/aes256-gcm96/rotate
+
순환되어 새로 추가된 키 버전을 확인한다.
$ vault read keymgmt/key/aes256-gcm96
+Key Value
+--- -----
+deletion_allowed false
+latest_version 2
+min_enabled_version 1
+name aes256-gcm96
+type aes256-gcm96
+versions map[1:map[creation_time:2023-07-05T12:04:48.099141545Z] 2:map[creation_time:2023-07-05T12:23:01.870942633Z]]
+
키가 적용된 AWS KMS에도 순환된 키 버전이 적용됨을 확인한다.
$ vault read keymgmt/kms/aws-kms-anea2/key/aes256-gcm96
+Key Value
+--- -----
+distribution_time 2023-07-05T12:14:52.173163449Z
+name aes256-gcm96-1688559292
+protection hsm
+purpose decrypt,encrypt
+versions map[1:503e44bf-7629-47c3-8c22-a5337b3aab3a 2:c2bb1927-76b2-4fce-8b32-73569489a70c]
+
추가된 마지막 키는 별칭(Alias)로 지정되어 앱에서는 alias/hashicorp/<name>
으로 호출할 수 있다.
(e.g. alias/hashicorp/aes256-gcm96-1688602383
)
AWS Console에서 확인하면, 신규 키 ID
가 적용된 항목이 새로 추가됨을 확인할 수 있다.
적용된 키의 최소 버전을 업데이트 한다.
$ vault write keymgmt/key/aes256-gcm96 min_enabled_version=2 deletion_allowed=true
+
키의 최소 버전에 따라 그 이하의 키가 삭제되었음을 확인한다. (비활성 처리)
$ vault read keymgmt/key/aes256-gcm96
+Key Value
+--- -----
+deletion_allowed true
+latest_version 2
+min_enabled_version 2
+name aes256-gcm96
+type aes256-gcm96
+versions map[2:map[creation_time:2023-07-05T12:23:01.870942633Z]]
+
AWS KMS에 적용된 버전은 기존 버전은 존재하나, AWS Console에서 확인하면 비활성 처리됨을 확인할 수 있다.
$ vault read keymgmt/kms/aws-kms-anea2/key/aes256-gcm96
+Key Value
+--- -----
+distribution_time 2023-07-05T12:14:52.173163449Z
+name aes256-gcm96-1688559292
+protection hsm
+purpose decrypt,encrypt
+versions map[1:503e44bf-7629-47c3-8c22-a5337b3aab3a 2:c2bb1927-76b2-4fce-8b32-73569489a70c]
+
최소 버전을 이전 버전을 포함하여 업데이트 하면 이전 버전의 키가 복구된다.
$ vault write keymgmt/key/aes256-gcm96 min_enabled_version=1 deletion_allowed=true
+Success! Data written to: keymgmt/key/aes256-gcm96
+
+$ vault read keymgmt/key/aes256-gcm96
+Key Value
+--- -----
+deletion_allowed true
+latest_version 2
+min_enabled_version 1
+name aes256-gcm96
+type aes256-gcm96
+versions map[1:map[creation_time:2023-07-05T12:04:48.099141545Z] 2:map[creation_time:2023-07-05T12:23:01.870942633Z]]
+
AWS KMS 구성에서 키를 삭제하는 경우 적용된 키가 일괄 삭제 대기
상태로 변경되며, 해당 키는 삭제 가능하다. (AWS 정책상 7~30일 유예)
$ vault delete keymgmt/kms/aws-kms-anea2/key/aes256-gcm96
+Success! Data deleted (if it existed) at: keymgmt/kms/aws-kms-anea2/key/aes256-gcm96
+
AWS KMS에서 생성된 암호화 키는 기본적으로 해당 리전에 한정되어 사용된다. 암호화 키는 생성된 리전 내에서만 사용 가능하며, 다른 리전에 직접 이동시키는 것은 불가능하다. 이는 AWS KMS 서비스의 설계와 보안 모델에 기인한 제약 사항이다.
AWS KMS는 키 관리와 관련된 강력한 보안 제어를 제공합니다. 이를 위해 암호화 키는 해당 리전의 KMS 서비스에 의해 엄격하게 관리되며, 다른 리전에 암호화 키를 이동시키는 것은 보안상의 이슈를 야기할 수 있어 기본 구성은 아니다.
따라서 암호화 키를 다른 AWS 리전의 KMS에 적용하려면, 해당 리전에서 새로운 암호화 키를 생성해야 합니다. 이는 암호화 키의 보안성과 범위를 유지하기 위해 필요한 조치이다.
만약 여러 리전에서 동일한 암호화 키를 사용해야 하는 경우에는 AWS KMS의 Cross-Region Replication 기능을 사용해야 한다.
키 순환을 위해서는 데이터 암호화 시 복호화 가능한 AWS KMS의 Id 또는 arn을 함께 기록해야 복호화 시 사용할 키를 지정할 수 있다. 볼트의 transit
과는 달리 새로 생성된 키는 기존 암호화된 데이터를 복호화 할 수 없다.
boto3
와 pycryptodome
패키$ python --version
+Python 3.9.12
+
+$ pip install boto3
+$ pip install pycryptodome
+
import base64
+import json
+import logging
+import boto3
+from botocore.exceptions import ClientError
+AWS_REGION = 'ap-northeast-2'
+
+logger = logging.getLogger()
+logging.basicConfig(level=logging.INFO, format='%(asctime)s: %(levelname)s: %(message)s')
+kms_client = boto3.client("kms", region_name=AWS_REGION)
+
+def encrypt(secret, alias):
+ """
+ Encrypts plaintext into ciphertext by using a KMS key.
+ """
+ try:
+ cipher_text = kms_client.encrypt(
+ KeyId=alias,
+ Plaintext=bytes(secret, encoding='utf8'),
+ )
+ except ClientError:
+ logger.exception('Could not encrypt the string.')
+ raise
+ else:
+ return base64.b64encode(cipher_text["CiphertextBlob"])
+
+if __name__ == '__main__':
+ Constants
+ SECRET = 'hands-on-vault-key-management'
+ KEY_ALIAS = 'alias/hashicorp/aes256-gcm96-1688605574'
+ logger.info('Encrypting...')
+ kms = encrypt(SECRET, KEY_ALIAS)
+ logger.info(f'Encrypted string: {kms}.')
+
CIPHER_BLOB
에 붙여넣어 테스트import base64
+import json
+import logging
+import boto3
+from botocore.exceptions import ClientError
+AWS_REGION = 'ap-northeast-2'
+
+logger = logging.getLogger()
+logging.basicConfig(level=logging.INFO, format='%(asctime)s: %(levelname)s: %(message)s')
+kms_client = boto3.client("kms", region_name=AWS_REGION)
+
+def decrypt(cipher_text, alias):
+ """
+ Decrypts ciphertext that was encrypted by a KMS key.
+ """
+ try:
+ plain_text = kms_client.decrypt(KeyId=alias, CiphertextBlob=bytes(base64.b64decode(cipher_text)))
+ except ClientError:
+ logger.exception('Could not decrypt the string.')
+ raise
+ else:
+ return plain_text['Plaintext']
+
+if __name__ == '__main__':
+ Constants
+ CIPHER_BLOB = 'AQICAHgGLc+TNQuAGEYhHYwf5zxQz9XvN0uXI2N4YU+dPYN0fgFmLVCFrkLNP+EJWytolEIfAAAAezB5BgkqhkiG9w0BBwagbDBqAgEAMGUGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMn0LDunt5nrftC18BAgEQgDgPSUhp2iLAGjEFUuSOSxDdYj1m9o4KetZJjmKfX4pvvZMJGkozLEnZpQ0KMET5NjjyGOzax7H84g=='
+ KEY_ALIAS = 'alias/hashicorp/aes256-gcm96-1688605574'
+ logger.info('Decrypting...')
+ kms = decrypt(CIPHER_BLOB, KEY_ALIAS)
+ logger.info(f"Decrypted string: {kms.decode('utf8')}.")
+
https://developer.hashicorp.com/vault/tutorials/adp/key-management-secrets-engine-azure-key-vault
Azure Key Vault
키 자격 증명 모음
에서 새 키 생성Azure Key Vault 관리를 위한 권한
create
delete
get
import
update
Terraform Example
terraform {
+ required_providers {
+ azurerm = {
+ source = "hashicorp/azurerm"
+ version = "~> 3.65.0"
+ }
+ }
+}
+
+provider "azurerm" {
+ features {
+ key_vault {
+ purge_soft_delete_on_destroy = true
+ recover_soft_deleted_key_vaults = true
+ }
+ }
+}
+
+resource "random_id" "app_rg_name" {
+ byte_length = 3
+}
+
+data "azurerm_client_config" "current" {}
+
+resource "azurerm_resource_group" "key_vault_rg" {
+ name = "gs-rg-${random_id.app_rg_name.hex}"
+ location = "Korea Central"
+}
+
+resource "azurerm_key_vault" "example" {
+ name = "gs-keyvault-${random_id.app_rg_name.hex}-vault"
+ location = azurerm_resource_group.key_vault_rg.location
+ resource_group_name = azurerm_resource_group.key_vault_rg.name
+ sku_name = "premium"
+ soft_delete_retention_days = 7
+ tenant_id = data.azurerm_client_config.current.tenant_id
+
+ access_policy {
+ tenant_id = data.azurerm_client_config.current.tenant_id
+ object_id = data.azurerm_client_config.current.object_id
+
+ // "Create", "Delete", "Get", "Import", "Update"
+ key_permissions = [
+ "Backup", "Create", "Decrypt", "Delete", "Encrypt", "Get", "Import", "List",
+ "Purge", "Recover", "Restore", "Sign", "UnwrapKey", "Update", "Verify", "WrapKey",
+ "Release", "Rotate", "GetRotationPolicy"
+ ]
+ }
+}
+
+output "key_vault_name" {
+ value = azurerm_key_vault.example.name
+}
+
Azure Key Vault에서 호환되는 암호화 키를 생성한다.
rsa-2048
, rsa-3072
, rsa-4096
$ vault write -f keymgmt/key/rsa-2048-key type="rsa-2048"
+Success! Data written to: keymgmt/key/rsa-2048-key
+
생성된 암호화 키의 정보를 확인한다.
$ vault read keymgmt/key/rsa-2048-key
+
+Key Value
+--- -----
+deletion_allowed false
+latest_version 1
+min_enabled_version 1
+name rsa-2048-key
+type rsa-2048
+versions map[1:map[creation_time:2023-07-14T19:18:28.45274+09:00 public_key:-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2qK54OiinWQFdyupvkg0
+HqBPpp/5H29fhcByipoEMpCMHpqNwgea2r6I3sTWX/0YdLZ6w/1L4Fc+B/yABu66
+vXq31OXvnIkvkT73jn9qEQsnYIhqdnElngT+4DOD5nuxPd4e8Ov5OOCIAjKA36YI
+VRiTJtR36qUFFVxxByGnvgSZ3Q090bRRLZx0WidqUilDAjh9CFucAcl3ybl5F80U
+H3aA9HiakGm+hTV1PLZPOT9mhmZk92NFSRVEuEddNb7Rndg3RrZ2/Sgrlbmxc28R
+SJnQswhA9Qbb9HmjCEmfo3rXpvEzJy8YCY24nk5GsyOwOA9z5uQwEJidBxmpsvdy
+QQIDAQAB
+-----END PUBLIC KEY-----
+]]
+
Azure Key Vault 공급자 리소스를 구성한다.
기본 Location은 "West US"
+$ export AZURE_LOCATION='koreacentral'
+
+$ vault write keymgmt/kms/gs-keyvault-mgmt \
+ key_collection="gs-keyvault-ee81ec-vault" \
+ provider="azurekeyvault" \
+ credentials=client_id=$AZURE_CLIENT_ID \
+ credentials=client_secret=$AZURE_CLIENT_SECRET \
+ credentials=tenant_id=$AZURE_TENANT_ID
+
+Success! Data written to: keymgmt/kms/gs-keyvault-mgmt
+
Azure Key Vault 공급자 리소스를 구성하는 매개변수의 설명은 다음과 같다. credentials은 Managed Service Identity (MSI)가 구성된 Azure 상에서 Vault가 실행되거나 환경변수로 지정된 경우 생략 가능하다.
매개변수 | 설명 |
---|---|
provider | AWS KMS를 구성하는 경우 awskms 를 사용 |
key_collection | 기존 Azure Key Vault 인스턴스의 이름을 나타냅니다. 생성 후에는 변경할 수 없습니다. |
credentials=client_id | Azure API를 호출하기 위한 자격 증명을 위한 클라이언트 ID (AZURE_CLIENT_ID ) |
credentials=client_secret | Azure API를 호출하기 위한 자격 증명을 위한 클라이언트 암호 (AZURE_CLIENT_SECRET ) |
credentials=tenant_id | Azure Active Directory 조직의 테넌트 ID (AZURE_TENANT_ID ) |
Azure Key Vault 공급자 리소스에 암호화 키를 배포
vault write keymgmt/kms/:name/key/:key_name
+$ vault write keymgmt/kms/gs-keyvault-mgmt/key/rsa-2048-key \
+ purpose="encrypt,decrypt" \
+ protection="hsm"
+
+Success! Data written to: keymgmt/kms/gs-keyvault-mgmt/key/rsa-2048-key
+
+$ vault write keymgmt/kms/gs-keyvault-mgmt/key/rsa-4096-sign \
+ purpose="sign" \
+ protection="hsm"
+
+Success! Data written to: keymgmt/kms/gs-keyvault-mgmt/key/rsa-4096-sign
+
현재 키 버전을 확인한다.
$ vault read keymgmt/kms/gs-keyvault-mgmt/key/rsa-2048-key
+Key Value
+--- -----
+distribution_time 2023-07-14T19:19:47.100453+09:00
+name rsa-2048-key-1689329987
+protection hsm
+purpose decrypt,encrypt
+versions map[1:80bb514c42f14422b3d3405d3b2fa1fd]
+
Azure Key Vault에 적용된 키를 순환시킨다.
$ vault write -f keymgmt/key/rsa-2048-key/rotate
+
순환되어 새로 추가된 키 버전을 확인한다.
$ vault read keymgmt/key/rsa-2048-key
+
+Key Value
+--- -----
+deletion_allowed false
+latest_version 2
+min_enabled_version 1
+name rsa-2048-key
+type rsa-2048
+versions map[1:map[creation_time:2023-07-14T19:18:28.45274+09:00 public_key:-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2qK54OiinWQFdyupvkg0
+HqBPpp/5H29fhcByipoEMpCMHpqNwgea2r6I3sTWX/0YdLZ6w/1L4Fc+B/yABu66
+vXq31OXvnIkvkT73jn9qEQsnYIhqdnElngT+4DOD5nuxPd4e8Ov5OOCIAjKA36YI
+VRiTJtR36qUFFVxxByGnvgSZ3Q090bRRLZx0WidqUilDAjh9CFucAcl3ybl5F80U
+H3aA9HiakGm+hTV1PLZPOT9mhmZk92NFSRVEuEddNb7Rndg3RrZ2/Sgrlbmxc28R
+SJnQswhA9Qbb9HmjCEmfo3rXpvEzJy8YCY24nk5GsyOwOA9z5uQwEJidBxmpsvdy
+QQIDAQAB
+-----END PUBLIC KEY-----
+] 2:map[creation_time:2023-07-14T20:14:02.507171+09:00 public_key:-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy+5ziHlHjaKN+YqfZX70
+5pxjVZqT4rq2ZkFAK+HRNbSW9QQltBpnn1hmyDEhZX8FAxTiaEpF01ZVptmrNY3Q
+KkHMduqUReA1jjcLbQ2E6DYCp3B/RUDLD7vNuXHvgGqTQr7aeEs0JHKYTERXt0MQ
+KUeFCBRi6zyAiTrcGU2o2/PRNs3Lmxjf88IFziDbcCj4Alqj1+0ruD0n1/HG6yXI
+1F5wYzziimJ+J4A3Sw2xQC/1tOxOR2onjMDT4Fd1xIsp3N7wKWFgGmQoZKn1ETtX
+e4m1ZLZEmrQnpz0aoiG1IXvwfa3ncjPhrhXM2f53p0r9Zuwuq4SZpg/ZRM1zd9No
+BQIDAQAB
+-----END PUBLIC KEY-----
+]]
+
키가 적용된 AWS KMS에도 순환된 키 버전이 적용됨을 확인한다.
$ vault read keymgmt/kms/gs-keyvault-mgmt/key/rsa-2048-key
+Key Value
+--- -----
+distribution_time 2023-07-14T19:19:47.100453+09:00
+name rsa-2048-key-1689329987
+protection hsm
+purpose decrypt,encrypt
+versions map[1:80bb514c42f14422b3d3405d3b2fa1fd 2:cb4765bae58b40e8bc1d77a96f0c0079]
+
추가된 마지막 키는 key_id로 지정되어 앱에서는 https://<kms이름>.vault.azure.net/keys/<key이름>/<적용된 key 버전>
으로 호출할 수 있다.
(e.g. https://gs-keyvault-ee81ec-vault.vault.azure.net/keys/rsa-2048-key-1689329987/80bb514c42f14422b3d3405d3b2fa1fd
)
Azure Console에서 확인하면, 신규 키 ID
가 적용된 항목이 새로 추가됨을 확인할 수 있다.
적용된 키의 최소 버전을 업데이트 한다.
$ vault write keymgmt/key/rsa-2048-key min_enabled_version=2 deletion_allowed=true
+
키의 최소 버전에 따라 그 이하의 키가 삭제되었음을 확인한다. (비활성 처리)
$ vault read keymgmt/key/rsa-2048-key
+Key Value
+--- -----
+deletion_allowed true
+latest_version 2
+min_enabled_version 2
+name rsa-2048-key
+type rsa-2048
+versions map[2:map[creation_time:2023-07-14T20:14:02.507171+09:00 public_key:-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy+5ziHlHjaKN+YqfZX70
+5pxjVZqT4rq2ZkFAK+HRNbSW9QQltBpnn1hmyDEhZX8FAxTiaEpF01ZVptmrNY3Q
+KkHMduqUReA1jjcLbQ2E6DYCp3B/RUDLD7vNuXHvgGqTQr7aeEs0JHKYTERXt0MQ
+KUeFCBRi6zyAiTrcGU2o2/PRNs3Lmxjf88IFziDbcCj4Alqj1+0ruD0n1/HG6yXI
+1F5wYzziimJ+J4A3Sw2xQC/1tOxOR2onjMDT4Fd1xIsp3N7wKWFgGmQoZKn1ETtX
+e4m1ZLZEmrQnpz0aoiG1IXvwfa3ncjPhrhXM2f53p0r9Zuwuq4SZpg/ZRM1zd9No
+BQIDAQAB
+-----END PUBLIC KEY-----
+]]
+
Azure Key Vault에 적용된 버전은 기존 버전은 존재하나, AWS Console에서 확인하면 비활성 처리됨을 확인할 수 있다.
$ vault read keymgmt/kms/gs-keyvault-mgmt/key/rsa-2048-key
+Key Value
+--- -----
+distribution_time 2023-07-14T19:19:47.100453+09:00
+name rsa-2048-key-1689329987
+protection hsm
+purpose decrypt,encrypt
+versions map[1:80bb514c42f14422b3d3405d3b2fa1fd 2:cb4765bae58b40e8bc1d77a96f0c0079]
+
최소 버전을 이전 버전을 포함하여 업데이트 하면 이전 버전의 키가 복구된다.
$ vault write keymgmt/key/rsa-2048-key min_enabled_version=1 deletion_allowed=true
+Success! Data written to: keymgmt/key/aes256-gcm96
+
+$ vault read keymgmt/key/rsa-2048-key
+Key Value
+--- -----
+deletion_allowed true
+latest_version 2
+min_enabled_version 1
+name rsa-2048-key
+type rsa-2048
+versions map[1:map[creation_time:2023-07-14T19:18:28.45274+09:00 public_key:-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2qK54OiinWQFdyupvkg0
+HqBPpp/5H29fhcByipoEMpCMHpqNwgea2r6I3sTWX/0YdLZ6w/1L4Fc+B/yABu66
+vXq31OXvnIkvkT73jn9qEQsnYIhqdnElngT+4DOD5nuxPd4e8Ov5OOCIAjKA36YI
+VRiTJtR36qUFFVxxByGnvgSZ3Q090bRRLZx0WidqUilDAjh9CFucAcl3ybl5F80U
+H3aA9HiakGm+hTV1PLZPOT9mhmZk92NFSRVEuEddNb7Rndg3RrZ2/Sgrlbmxc28R
+SJnQswhA9Qbb9HmjCEmfo3rXpvEzJy8YCY24nk5GsyOwOA9z5uQwEJidBxmpsvdy
+QQIDAQAB
+-----END PUBLIC KEY-----
+] 2:map[creation_time:2023-07-14T20:14:02.507171+09:00 public_key:-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy+5ziHlHjaKN+YqfZX70
+5pxjVZqT4rq2ZkFAK+HRNbSW9QQltBpnn1hmyDEhZX8FAxTiaEpF01ZVptmrNY3Q
+KkHMduqUReA1jjcLbQ2E6DYCp3B/RUDLD7vNuXHvgGqTQr7aeEs0JHKYTERXt0MQ
+KUeFCBRi6zyAiTrcGU2o2/PRNs3Lmxjf88IFziDbcCj4Alqj1+0ruD0n1/HG6yXI
+1F5wYzziimJ+J4A3Sw2xQC/1tOxOR2onjMDT4Fd1xIsp3N7wKWFgGmQoZKn1ETtX
+e4m1ZLZEmrQnpz0aoiG1IXvwfa3ncjPhrhXM2f53p0r9Zuwuq4SZpg/ZRM1zd9No
+BQIDAQAB
+-----END PUBLIC KEY-----
+]]
+
Azure Key Vault 구성에서 키를 삭제하는 경우 적용된 키가 일괄 삭제된다.
$ vault delete keymgmt/kms/gs-keyvault-mgmt/key/rsa-2048-key
+Success! Data deleted (if it existed) at: keymgmt/kms/gs-keyvault-mgmt/key/rsa-2048-key
+
키 순환을 위해서는 데이터 암호화 시 복호화 가능한 AWS KMS의 Id 또는 arn을 함께 기록해야 복호화 시 사용할 키를 지정할 수 있다. 볼트의 transit
과는 달리 새로 생성된 키는 기존 암호화된 데이터를 복호화 할 수 없다.
azure-keyvault-keys
, azure-keyvault-secrets
, azure-identity
패키지aiohttp
패키지$ python --version
+Python 3.9.12
+
+$ pip install azure-keyvault-keys azure-keyvault-secrets azure-identity aiohttp
+
from azure.identity import DefaultAzureCredential
+from azure.keyvault.keys import KeyClient
+
+credential = DefaultAzureCredential()
+key_client = KeyClient(vault_url="https://gs-keyvault-ee81ec-vault.vault.azure.net/", credential=credential)
+keys = key_client.list_properties_of_keys()
+
+for key in keys:
+ print(key.name)
+
$ python vault_list.py
+
+rsa-2048-key-1689329987
+rsa-4096-sign-1689330068
+
from azure.identity import DefaultAzureCredential
+from azure.keyvault.keys.crypto import CryptographyClient
+
+ Azure Key Vault 관련 설정
+key_vault_name = "gs-keyvault-ee81ec-vault"
+key_id = "https://gs-keyvault-ee81ec-vault.vault.azure.net/keys/rsa-2048-key-1689329987/80bb514c42f14422b3d3405d3b2fa1fd"
+
+ 인증 및 액세스 토큰 가져오기
+credential = DefaultAzureCredential()
+
+ Key Vault 클라이언트 및 암호화 클라이언트 생성
+cryptography_client = CryptographyClient(key=key_id, credential=credential)
+
+ 암호화할 데이터
+data_to_encrypt = b"Hello, Azure Key Vault!"
+
+ 데이터 암호화
+result = cryptography_client.encrypt(algorithm="RSA-OAEP", plaintext=data_to_encrypt)
+encrypted_data = result.ciphertext
+
+print("암호화된 데이터:", encrypted_data)
+
$ python encrypt.py
+Local encrypt operation failed: 'str' object has no attribute 'value'
+암호화된 데이터: b"Q\x8e\x94\xe0R\xd7\xc5\x87\xa4M\x9cMx\xccM\xc2S\xd5C\xe0\xef7\xf5\x1afJ8A\x81\xef\xfcA\x8b\x82\xb4\x8d\x93\x17\xa7\xc5\x0b?x\x9b\xa6\xfd\xc2qe<_\x99@yC\x16\xa6\xcbSn\x10Z\xa9y\xa6\xf5V\xdd\xdc\x9c\xe7\xf2\x0fs\x9b\x06j\r+z\x11D|lu\xce\xccV\x9b\xef\xb8\x9c\xc2\x9b7A>\xff\xf8\x806\x98\x00o.|):\xea\x9a\xbcI\x92b\x81DE|\xc1\x80\xae\xbb\x7f\xc9\x8e5\xc5\t|\xe8\xc8\xac\x1d\x98\xc7\xc0\xca\x00b\n\x13\xe4\xd1j\xe6]L\xff'\xb7\xbd^g\xb4\x9eAZq#\x9c\x10A\x83\x82\x9d\x1bXR\xba\xb6\x17\xc3&\xaa\x95l\x83\xfcs\x89)\xb1\xde^\x07\xb3s\x87\x90\xfd\x83\xf0\xfc\x15\x82\x1a\x02\xb1\x93\x8e\x1d\x88u!K\xc9y\xdfL^\x97\xe5\xb5\x05\x83\xe4!E1\x83k\x11\xceC}\xb0C{Td\xa1\x8a\x0f=\xbeE'\x0c7\x14\xbfKm\xd0I}\xb9\xb9P\x93\xb3$\xa33\xfdn"
+
data_to_decrypt
에 붙여넣어 테스트from azure.identity import DefaultAzureCredential
+from azure.keyvault.keys.crypto import CryptographyClient
+
+ Azure Key Vault 관련 설정
+key_vault_name = "gs-keyvault-ee81ec-vault"
+key_id = "https://gs-keyvault-ee81ec-vault.vault.azure.net/keys/rsa-2048-key-1689329987/80bb514c42f14422b3d3405d3b2fa1fd"
+
+ 인증 및 액세스 토큰 가져오기
+credential = DefaultAzureCredential()
+
+ Key Vault 클라이언트 및 암호화 클라이언트 생성
+cryptography_client = CryptographyClient(key=key_id, credential=credential)
+
+ 복호화할 데이터 (b"Hello, Azure Key Vault!")
+data_to_decrypt = b"Q\x8e\x94\xe0R\xd7\xc5\x87\xa4M\x9cMx\xccM\xc2S\xd5C\xe0\xef7\xf5\x1afJ8A\x81\xef\xfcA\x8b\x82\xb4\x8d\x93\x17\xa7\xc5\x0b?x\x9b\xa6\xfd\xc2qe<_\x99@yC\x16\xa6\xcbSn\x10Z\xa9y\xa6\xf5V\xdd\xdc\x9c\xe7\xf2\x0fs\x9b\x06j\r+z\x11D|lu\xce\xccV\x9b\xef\xb8\x9c\xc2\x9b7A>\xff\xf8\x806\x98\x00o.|):\xea\x9a\xbcI\x92b\x81DE|\xc1\x80\xae\xbb\x7f\xc9\x8e5\xc5\t|\xe8\xc8\xac\x1d\x98\xc7\xc0\xca\x00b\n\x13\xe4\xd1j\xe6]L\xff'\xb7\xbd^g\xb4\x9eAZq#\x9c\x10A\x83\x82\x9d\x1bXR\xba\xb6\x17\xc3&\xaa\x95l\x83\xfcs\x89)\xb1\xde^\x07\xb3s\x87\x90\xfd\x83\xf0\xfc\x15\x82\x1a\x02\xb1\x93\x8e\x1d\x88u!K\xc9y\xdfL^\x97\xe5\xb5\x05\x83\xe4!E1\x83k\x11\xceC}\xb0C{Td\xa1\x8a\x0f=\xbeE'\x0c7\x14\xbfKm\xd0I}\xb9\xb9P\x93\xb3$\xa33\xfdn"
+
+ 데이터 암호화
+result = cryptography_client.decrypt(algorithm="RSA-OAEP", ciphertext=data_to_decrypt)
+decrypted_data = result.plaintext
+
+print("복호화된 데이터:", decrypted_data)
+
$ python decrypt.py
+복호화된 데이터: b'Hello, Azure Key Vault!'
+
https://console.cloud.google.com 을 통해 GCP 포탈에 접속 한다.
상단 프로젝트 선택
에서 프로젝트 이름을 선택합니다.
좌측 메뉴확장 또는 검색을 통해 IAM 및 관리자 > 역할
을 선택한다.
+ 역할 만들기
선택하여 Vault Key Management를 위한 역할 생성
제목 : e.g. vault-key-management
Vault Key Management를 위해 할당된 권한
에 다음을 추가
cloudkms.cryptoKeys.create
cloudkms.cryptoKeys.update
cloudkms.importJobs.create
cloudkms.importJobs.get
cloudkms.cryptoKeyVersions.list
cloudkms.cryptoKeyVersions.destroy
cloudkms.cryptoKeyVersions.update
cloudkms.cryptoKeyVersions.create
cloudkms.importJobs.useToImport
KeyRing을 생성하기 위해서는 할당된 권한
에 다음을 추가
Encrypt/Decrypt를 테스트하기 위해서는 할당된 권한
에 다음을 추가
Vault가 Cloud KMS 인스턴스에 연결하고 관리하는 데 사용할 이 서비스 계정에 대한 JSON 기반 자격 증명 파일을 생성해야한다.
프로젝트 선택
에서 프로젝트 이름을 선택합니다.IAM 및 관리자 > 서비스 계정
을 선택한다.+ 서비스 계정 만들기
를 선택하여 신규 계정을 추가한 뒤 속성을 부여 하고 완료 합니다. vault-keymgmt-test
vault-key-management
키
탭을 선택합니다.키 추가
드롭다운 메뉴를 클릭하고 새 키 만들기
를 선택합니다.키 유형
은 JSON 을 선택하여 생성합니다.Vault의 Key Management에서 관리할 GCP Cloud KMS를 생성한다.
좌측 메뉴확장 또는 검색을 통해 `보안 > Key Management를 선택한다.
Cloud KMS가 비활성인경우 사용
버튼으로 활성화 한다.
GCP 안내에 따라 새로운 키링을 생성 한다. (https://cloud.google.com/kms/docs/create-encryption-keys?hl=ko)
또는 Terraform 으로 새로운 키링을 생성 한다.
terraform {
+ required_providers {
+ google = {
+ source = "hashicorp/google"
+ version = "~> 4.73.1"
+ }
+ }
+}
+
+locals {
+ region = "asia-northeast3"
+}
+
+provider "google" {
+ project = "hc-f5e09ac82cca41c78e99aac5ea3"
+ credentials = file("kms.json")
+ region = local.region
+}
+
+resource "google_kms_key_ring" "keyring" {
+ name = "vault-keyring"
+ location = local.region
+}
+
생성된 키링의 리소스 이름 복사
를 클릭하면 Vault 구성에서 사용할 key_collection
에 할당하는 키링의 이름을 복사할 수 있다.
GCP KMS에서 호환되는 암호화 키를 생성한다.
$ vault write -f keymgmt/key/gcp-aes256-gcm96 type="aes256-gcm96"
+Success! Data written to: keymgmt/key/gcp-aes256-gcm96
+
생성된 암호화 키의 정보를 확인한다.
$ vault read keymgmt/key/gcp-aes256-gcm96
+
+Key Value
+--- -----
+deletion_allowed false
+latest_version 1
+min_enabled_version 1
+name gcp-aes256-gcm96
+type aes256-gcm96
+versions map[1:map[creation_time:2023-07-17T13:46:58.17194+09:00]]
+
GCP Cloud KMS 공급자 리소스를 구성한다.
$ vault write keymgmt/kms/gcpckms-korea \
+ provider="gcpckms" \
+ key_collection="projects/hc-f5e09ac82cca41c78e99aac5ea3/locations/asia-northeast3/keyRings/vault-keyring" \
+ credentials=service_account_file="$FULL_PATH/kms.json"
+
+Success! Data written to: keymgmt/kms/gcpckms-korea
+
AWS KMS 공급자 리소스를 구성하는 매개변수의 설명은 다음과 같다.
매개변수 | 설명 |
---|---|
provider | GCP Cloud KMS를 구성하는 경우 gcpckms 를 사용 |
key_collection | GCP Cloud KMS의 키링 리소스 이름을 지정 |
credentials=service_account_file | 자격증명 파일로 GCP Cloud KMS 인증에 사용할 자격증명 파일을 지정한다. GOOGLE_CREDENTIALS 로 지정된 경우 생략할 수 있다. |
AWS KMS 공급자 리소스에 암호화 키를 배포
vault write keymgmt/kms/:name/key/:key_name
+$ vault write keymgmt/kms/gcpckms-korea/key/gcp-aes256-gcm96 \
+ purpose="encrypt,decrypt" \
+ protection="hsm"
+
+Success! Data written to: keymgmt/kms/gcpckms-korea/key/gcp-aes256-gcm96
+
등록이 완료되면 GCP Console의 대상 키링에 키가 추가된 것을 확인할 수 있다.
생성된 키정보를 확인하여 현재 버전에 명시된 ID가 GCP 상의 KMS의 키 ID
와 같은지 확인한다.
$ vault read keymgmt/kms/gcpckms-korea/key/gcp-aes256-gcm96
+
+Key Value
+--- -----
+distribution_time 2023-07-17T14:48:10.777969+09:00
+name gcp-aes256-gcm96-1689572890
+protection hsm
+purpose decrypt,encrypt
+versions map[1:1]
+
GCP Cloud KMS사용자의 경우 vault로의 API 요청으로 키ID를 확인할 수 있다.
curl -H "X-Vault-Token: token" -X GET http://<vault_hostname>:<port>/v1/keymgmt/kms/gcpckms-korea/key/gcp-aes256-gcm96
+{
+ "request_id": "6f3a9711-2c6c-d894-55a9-74897d735759",
+ "lease_id": "",
+ "lease_duration": 0,
+ "renewable": false,
+ "data": {
+ "distribution_time": "2023-07-17T14:48:10.777969+09:00",
+ "name": "gcp-aes256-gcm96-1689572890",
+ "protection": "hsm",
+ "purpose": "decrypt,encrypt",
+ "versions": {
+ "1": "1"
+ }
+ },
+ "warnings": null
+}
+
GCP Cloud KMS에 적용된 키를 순환시킨다.
$ vault write -f keymgmt/key/gcp-aes256-gcm96/rotate
+
순환되어 새로 추가된 키 버전을 확인한다.
$ vault read keymgmt/key/gcp-aes256-gcm96
+Key Value
+--- -----
+deletion_allowed false
+latest_version 2
+min_enabled_version 1
+name gcp-aes256-gcm96
+type aes256-gcm96
+versions map[1:map[creation_time:2023-07-17T13:46:58.17194+09:00] 2:map[creation_time:2023-07-17T14:54:38.978298+09:00]]
+
키가 적용된 AWS KMS에도 순환된 키 버전이 적용됨을 확인한다.
$ vault read keymgmt/kms/gcpckms-korea/key/gcp-aes256-gcm96
+Key Value
+--- -----
+distribution_time 2023-07-17T14:48:10.777969+09:00
+name gcp-aes256-gcm96-1689572890
+protection hsm
+purpose decrypt,encrypt
+versions map[1:1 2:2]
+
GCP Console에서 확인하면, 신규 대상 키에 신규 버전의 키 항목이 새로 추가됨을 확인할 수 있다.
적용된 키의 최소 버전을 업데이트 한다.
$ vault write keymgmt/key/gcp-aes256-gcm96 min_enabled_version=2 deletion_allowed=true
+
+Success! Data written to: keymgmt/key/gcp-aes256-gcm96
+
키의 최소 버전에 따라 그 이하의 키가 삭제되었음을 확인한다. (비활성 처리)
$ vault read keymgmt/key/gcp-aes256-gcm96
+
+Key Value
+--- -----
+deletion_allowed true
+latest_version 2
+min_enabled_version 2
+name gcp-aes256-gcm96
+type aes256-gcm96
+versions map[2:map[creation_time:2023-07-17T14:54:38.978298+09:00]]
+
GCP Cloud KMS에 적용된 버전은 기존 버전은 존재하나, GCP Console에서 확인하면 비활성 처리됨을 확인할 수 있다.
$ vault read keymgmt/kms/gcpckms-korea/key/gcp-aes256-gcm96
+Key Value
+--- -----
+distribution_time 2023-07-17T14:48:10.777969+09:00
+name gcp-aes256-gcm96-1689572890
+protection hsm
+purpose decrypt,encrypt
+versions map[1:1 2:2]
+
최소 버전을 이전 버전을 포함하여 업데이트 하면 이전 버전의 키가 복구된다.
$ vault write keymgmt/key/gcp-aes256-gcm96 min_enabled_version=1 deletion_allowed=true
+
+Success! Data written to: keymgmt/key/gcp-aes256-gcm96
+
+$ vault read keymgmt/key/gcp-aes256-gcm96
+Key Value
+--- -----
+deletion_allowed true
+latest_version 2
+min_enabled_version 1
+name gcp-aes256-gcm96
+type aes256-gcm96
+versions map[1:map[creation_time:2023-07-17T13:46:58.17194+09:00] 2:map[creation_time:2023-07-17T14:54:38.978298+09:00]]
+
$ python --version
+Python 3.9.12
+
+$ pip install google-cloud-kms cryptography crcmod jwcrypto
+
+$ export GOOGLE_APPLICATION_CREDENTIALS="$FULL_PATH/kms.json"
+
google-cloud-kms 설치시 grpcio
설치 실패하여 테스트 하지 못함
https://github.com/googleapis/nodejs-kms/tree/aad6cc451952f42b96d752f31399a2c364f07610/samples
$ node --version
+v14.20.0
+
+$ npm install --save @google-cloud/kms
+$ npm install --save fast-crc32c
+
+$ export GOOGLE_APPLICATION_CREDENTIALS="$FULL_PATH/kms.json"
+
{
+ "name": "gcpckms-test",
+ "version": "1.0.0",
+ "description": "",
+ "main": "main.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "@google-cloud/kms": "^3.7.0",
+ "fast-crc32c": "^2.0.0"
+ }
+}
+
'use strict';
+
+const projectId = 'hc-f5e09ac82cca41c78e99aac5ea3'
+const locationId = 'asia-northeast3'
+const keyRingId = 'vault-keyring'
+const keyId = 'gcp-aes256-gcm96-1689572890'
+const versionId = '1'
+const plaintextBuffer = Buffer.from('Vault GCP Cloud KMS test')
+
+const {KeyManagementServiceClient} = require('@google-cloud/kms');
+
+const crc32c = require('fast-crc32c');
+
+// Instantiates a client
+const client = new KeyManagementServiceClient();
+
+// Build the key name
+const keyName = client.cryptoKeyPath(projectId, locationId, keyRingId, keyId, versionId);
+
+// Optional, but recommended: compute plaintext's CRC32C.
+async function encryptSymmetric() {
+ const plaintextCrc32c = crc32c.calculate(plaintextBuffer);
+ console.log(`Plaintext crc32c: ${plaintextCrc32c}`);
+ const [encryptResponse] = await client.encrypt({
+ name: keyName,
+ plaintext: plaintextBuffer,
+ plaintextCrc32c: {
+ value: plaintextCrc32c,
+ },
+ });
+
+ const ciphertext = encryptResponse.ciphertext;
+
+ // Optional, but recommended: perform integrity verification on encryptResponse.
+ // For more details on ensuring E2E in-transit integrity to and from Cloud KMS visit:
+ // https://cloud.google.com/kms/docs/data-integrity-guidelines
+ if (!encryptResponse.verifiedPlaintextCrc32c) {
+ throw new Error('Encrypt: request corrupted in-transit');
+ }
+ if (
+ crc32c.calculate(ciphertext) !==
+ Number(encryptResponse.ciphertextCrc32c.value)
+ ) {
+ throw new Error('Encrypt: response corrupted in-transit');
+ }
+
+ console.log(`Ciphertext: ${ciphertext.toString('base64')}`);
+ console.log(`Ciphertext crc32c: ${encryptResponse.ciphertextCrc32c.value}`)
+ return ciphertext;
+}
+
+async function decryptSymmetric(ciphertext) {
+ const cipherTextBuf = Buffer.from(await ciphertext);
+ const ciphertextCrc32c = crc32c.calculate(cipherTextBuf);
+ console.log(`Ciphertext crc32c: ${ciphertextCrc32c}`);
+ const [decryptResponse] = await client.decrypt({
+ name: keyName,
+ ciphertext: cipherTextBuf,
+ ciphertextCrc32c: {
+ value: ciphertextCrc32c,
+ },
+ });
+
+ // Optional, but recommended: perform integrity verification on decryptResponse.
+ // For more details on ensuring E2E in-transit integrity to and from Cloud KMS visit:
+ // https://cloud.google.com/kms/docs/data-integrity-guidelines
+ if (
+ crc32c.calculate(decryptResponse.plaintext) !==
+ Number(decryptResponse.plaintextCrc32c.value)
+ ) {
+ throw new Error('Decrypt: response corrupted in-transit');
+ }
+
+ const plaintext = decryptResponse.plaintext.toString();
+
+ console.log(`Plaintext: ${plaintext}`);
+ console.log(`Plaintext crc32c: ${decryptResponse.plaintextCrc32c.value}`)
+ return plaintext;
+}
+
+decryptSymmetric(encryptSymmetric());
+
$ node encrypt_decrypt.js
+Ciphertext: CiQADXWVuwUXBHPL+a8tqce4HfUe3YDMujDZebUWGn4wajmCflcSRypFChQKDJ2A64fX3MUmUfJ8fxDiwuqVBhITCgtm+dZClP/tuRw0CxDE64XfDRoYChChQEcfHsoXhHFXpkpaaTvMENuLg60G
+Plaintext: Vault GCP Cloud KMS test
+
Enterprise 기능
VAULT_UI=true vault server -dev-root-token-id=root -dev -log-level=trace
+
+export VAULT_ADDR="http://127.0.0.1:8200"
+echo "export VAULT_ADDR=$VAULT_ADDR" >> /root/.bashrc
+vault status
+vault login root
+
KMIP 활성화
$ vault secrets enable kmip
+
+Success! Enabled the kmip secrets engine at: kmip/
+
KMIP Listner 구성 (5696은 표준 기본 포트 입니다.)
$ vault write kmip/config listen_addrs=0.0.0.0:5696 \
+ tls_ca_key_type="rsa" \
+ tls_ca_key_bits=2048
+
+Success! Data written to: kmip/config
+
MongoDB에 전달할 KMIP CA 인증서를 저장
$ vault read -format json kmip/ca | jq -r .data.ca_pem > ca.pem
+
샘플로 "HashiCup" 앱의 관리 개체에 대한 범위를 생성
$ vault write -f kmip/scope/hashicups
+Success! Data written to: kmip/scope/hashicups
+
+$ vault write kmip/scope/hashicups/role/payments operation_all=true
+Success! Data written to: kmip/scope/hashicups/role/payments
+
리프 인증서와 개인 키 생성
$ vault write -format=json \
+ kmip/scope/hashicups/role/payments/credential/generate \
+ format=pem > credential.json
+$ jq -r .data.certificate < credential.json > cert.pem
+$ jq -r .data.private_key < credential.json > key.pem
+$ cat cert.pem key.pem > client.pem
+
KMIP을 사용하기 위한 옵션과 함께 mongoDB를 시작
$ mongod --dbpath /var/lib/mongodb \
+ --logpath /var/log/mongodb/mongo.log \
+ --enableEncryption \
+ --kmipServerName localhost \
+ --kmipPort 5696 \
+ --kmipServerCAFile ca.pem \
+ --kmipClientCertificateFile client.pem
+
KMIP 적용 확인
$ cat /var/log/mongodb/mongo.log | grep KMIP | jq
+{
+ "t": {
+ "$date": "2021-07-20T02:03:34.031+00:00"
+ },
+ "s": "I",
+ "c": "STORAGE",
+ "id": 24199,
+ "ctx": "initandlisten",
+ "msg": "Created KMIP key",
+ "attr": {
+ "keyId": "agZTSeeJyQjVOKJgn3xeGJ6Va8sXfRXP"
+ }
+}
+
샘플 데이터 삽입
$ mongo
+
+MongoDB Enterprise > db.examples.insertOne(
+ {
+ name: "sue",
+ age: 26
+ }
+)
+
+MongoDB Enterprise > exit
+
결과 확인
# Collection WiredTiger 파일에 기록된 정보
+# KMIP 적용 전
+$ cat /var/lib/mongodb/collection-7*
+A�#�\�D���1_id`�1�g�~R=��namesueage:@4�D��8�����D��2
+
+# KMIP 적용 후
+$ cat /var/lib/mongodb/collection-7*
+A�#�\
+ ��$�|��H�}l�����(ں?����s�ɛocD��\K�>J������m��N��#����_�������К
+�X���ϩ}_�z6��L�nQ���pQ�sO�]�0���h_� #�Ȟ�߳2
+
환경 변수
export VAULT_SKIP_VERIFY=True
+export VAULT_ADDR='http://172.28.128.11:8200'
+export VAULT_TOKEN=s.8YXFI825TZxnwLtYHsLc9Fnb
+
정책 및 사용자 구성
. ./<pki-policy.hcl>
$ vault policy write pki - << EOF
+# Enable secrets engine
+path "sys/mounts/*" {
+ capabilities = [ "create", "read", "update", "delete", "list" ]
+}
+
+# List enabled secrets engine
+path "sys/mounts" {
+ capabilities = [ "read", "list" ]
+}
+
+# Work with pki secrets engine
+path "pki*" {
+ capabilities = [ "create", "read", "update", "delete", "list", "sudo" ]
+}
+EOF
+
$ vault auth enable userpass
+
+$ vault write auth/userpass/users/pki \
+ password=pki \
+ policies=pki
+
+$ vault login -method userpass username=pki password=pki
+Success! You are now authenticated. The token information displayed below
+is already stored in the token helper. You do NOT need to run "vault login"
+again. Future Vault requests will automatically use this token.
+
+Key Value
+--- -----
+token s.ldJApybiqGBmq3CuBAaqsKXZ
+token_accessor Maek0IMLkOLmFVkpG4DoGUdY
+token_duration 768h
+token_renewable true
+token_policies ["pki"]
+identity_policies []
+policies ["pki"]
+token_meta_username db
+
+$ export VAULT_TOKEN=s.7mN7t6hd1a1m97j2ptytfCqf
+
$ vault auth enable approle
+Success! Enabled approle auth method at: approle/
+
+$ vault write auth/approle/role/pki-agent \
+ secret_id_ttl=120m \
+ token_ttl=60m \
+ token_max_tll=120m \
+ policies="pki"
+Success! Data written to: auth/approle/role/pki-agent
+
+$ vault read auth/approle/role/pki-agent/role-id
+Key Value
+--- -----
+role_id dfa2a248-1e1b-e2e9-200c-69c63b9ca447
+
+$ vault write -f auth/approle/role/pki-agent/secret-id
+Key Value
+--- -----
+secret_id 864360c1-c79f-ea7c-727b-7752361fe1ba
+secret_id_accessor 3cc068e2-a172-2bb1-c097-b777c3525ba6
+
+#Tip
+$ echo $(vault write -f -format=json auth/approle/role/pki-agent/secret-id | jq -r '.data.secret_id') > secretid
+
+$ vault write auth/approle/login role_id=dfa2a248-1e1b-e2e9-200c-69c63b9ca447 secret_id=864360c1-c79f-ea7c-727b-7752361fe1ba
+Key Value
+--- -----
+token s.uGtTFun8zSNcczBrtEJrSx5y
+token_accessor eLjxnLYqfVTWFbOCXDVqwb3S
+token_duration 1h
+token_renewable true
+token_policies ["default" "pki"]
+identity_policies []
+policies ["default" "pki"]
+token_meta_role_name pki-agent
+
pki secret engine 활성화
$ vault secrets enable -path pki pki
+Success! Enabled the database secrets engine at: pki/
+
최대 TTL (Time-to-Live)이 87600 시간(10년) 인 인증서를 발급
$ vault secrets tune -max-lease-ttl 87600h pki
+Success! Tuned the secrets engine at: pki/
+
루트 인증서 생성 CA_cert.crt
$ vault write -f -field=certificate pki/root/generate/internal \
+ common_name="example.com" \
+ ttl=87600h > CA_cert.crt
+
이렇게하면 새로운 자체 서명 된 CA 인증서와 개인 키가 생성됩니다. Vault는 임대 기간 (TTL)이 끝나면 생성 된 루트를 자동으로 취소합니다. CA 인증서는 자체 인증서 해지 목록 (CRL)에 서명합니다.
CA 와 CRL URL 구성
$ vault write pki/config/urls \
+ issuing_certificates="http://172.28.128.11:8200/v1/pki/ca" \
+ crl_distribution_points="http://172.28.128.11:8200/v1/pki/crl"
+
이전 단계에서 생성한 Root CA를 사용하여 Intermediate CA 생성
pki secret engine 활성화
$ vault secrets enable -path=pki_int pki
+Success! Enabled the pki secrets engine at: pki_int/
+
최대 TTL (Time-to-Live)이 43800 시간(5년) 인 인증서를 발급 하도록 비밀 엔진을 조정
$ vault secrets tune -max-lease-ttl=43800h pki_int
+Success! Tuned the secrets engine at: pki_int/
+
Intermediate CSR 생성 <pki_intermediate.csr>
$ vault write -format=json pki_int/intermediate/generate/internal \
+ common_name="example.com Intermediate Authority" \
+ | jq -r '.data.csr' > pki_intermediate.csr
+
Root 인증서로 Intermediate 인증서에 서명 <intermediate.cert.pem>
$ vault write -format=json pki/root/sign-intermediate csr=@pki_intermediate.csr \
+ format=pem_bundle ttl="43800h" \
+ | jq -r '.data.certificate' > intermediate.cert.pem
+
CSR이 서명되고 Root CA가 인증서를 반환하면 다시 Vault에서 가져옴
$ vault write -f pki_int/intermediate/set-signed certificate=@intermediate.cert.pem
+Success! Data written to: pki_int/intermediate/set-signed
+
URL 구성
$ vault write pki_int/config/urls \
+ issuing_certificates="http://172.28.128.11:8200/v1/pki_int/ca" \
+ crl_distribution_points="http://172.28.128.11:8200/v1/pki_int/crl"
+
Role은 자격 증명을 생성하는데 사용되는 정책에 매핑되는 논리적 이름으로, 구성 매개변수가 인증서 일반 이름, 대체 이름, 유효한 키 사용등을 제어 가능
Param | Description |
---|---|
allowed_domains | 역할의 도메인을 지정합니다 (allow_bare_domains 및allow-subdomains 옵션과 함께 사용). |
allow_bare_domains | 클라이언트가 실제 도메인 자체의 값과 일치하는 인증서를 요청할 수 있는지 여부를 지정합니다. |
allow_subdomains | 클라이언트가 다른 역할 옵션에서 허용하는 CN의 하위 도메인 인 CN을 사용하여 인증서를 요청할 수 있는지 여부를 지정합니다 (참고 : 여기에는 와일드 카드 하위 도메인이 포함됨). |
allow_glob_domains | allowed_domains에 지정된 이름에 glob 패턴 (예 : ftp * .example.com)을 포함 할 수 있습니다. |
여기서는 example-dot-com 이라는 Role 을 생성
하위 도메인을 허용하는 example-dot.com
Role 생성
$ vault write pki_int/roles/example-dot-com \
+ allowed_domains="example.com" \
+ allow_subdomains=true \
+ max_ttl="720h"
+Success! Data written to: pki_int/roles/example-dot-com
+
Vault의 단기 비밀관리의 철학은 인증서 수명을 짧게 유지하는 것입니다.
example-dot-com
Role에 따라 test.example.com
도메인에 대한 새 인증서 요청
(응답에는 PEM으로 인코딩 된 개인 키, 키 유형 및 인증서 일련 번호가 포함됩니다.)
$ vault write pki_int/issue/example-dot-com common_name="test.example.com" ttl="2m" #format="pem_bundle"
+# vault write pki_int/issue/example-dot-com common_name="tfe.example.com" ttl="700h"
+
+Key Value
+--- -----
+certificate -----BEGIN CERTIFICATE-----
+MIIDwzCCAqugAwIBAgIUTQABMCAsXjG6ExFTX8201xKVH4IwDQYJKoZIhvcNAQEL
+BQAwGjEYMBYGA1UEAxMPd3d3LmV4YW1wbGUuY29tMB4XDTE4MDcyNDIxMTMxOVoX
+ ...
+
+-----END CERTIFICATE-----
+issuing_ca -----BEGIN CERTIFICATE-----
+MIIDQTCCAimgAwIBAgIUbMYp39mdj7dKX033ZjK18rx05x8wDQYJKoZIhvcNAQEL
+ ...
+
+-----END CERTIFICATE-----
+private_key -----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAte1fqy2Ekj+EFqKV6N5QJlBgMo/U4IIxwLZI6a87yAC/rDhm
+W58liadXrwjzRgWeqVOoCRr/B5JnRLbyIKBVp6MMFwZVkynEPzDmy0ynuomSfJkM
+ ...
+
+-----END RSA PRIVATE KEY-----
+private_key_type rsa
+serial_number 4d:00:01:30:20:2c:5e:31:ba:13:11:53:5f:cd:b4:d7:12:95:1f:82
+
생성되는 PKI인증서를 자동으로 갱신하기 위해 Vault Agent 구성
secretid 생성의 예
echo $(vault write -f -format=json auth/approle/role/pki-agent/secret-id | jq -r '.data.secret_id') > secretid
+
[vault_agent.hcl]
pid_file = "/root/vault_agent/pidfile"
+
+auto_auth {
+ method {
+ type = "approle"
+ config = {
+ role_id_file_path = "/root/vault_agent/roleid"
+ secret_id_file_path = "/root/vault_agent/secretid"
+ }
+ }
+
+ sink {
+ type = "file"
+ config = {
+ path = "/tmp/vault_agent"
+ }
+ }
+}
+
+vault {
+ address = "http://172.28.128.11:8200"
+}
+
+template {
+ source = "/root/vault_agent/cert.tpl"
+ destination = "/root/cert/my-app.crt"
+}
+
+template {
+ source = "/root/vault_agent/ca.tpl"
+ destination = "/root/cert/ca.crt"
+}
+
+template {
+ source = "/root/vault_agent/key.tpl"
+ destination = "/root/cert/my-app.key"
+}
+
[인증 정보]
role_id_file_path
, secret_id_file_path
에는 앞서 생성한 approle의 role id와 secret id를 대상 파일에 저장
[template - default pem]
cert.tpl
{{- /* /tmp/ca.tpl */ -}}
+{{ with secret "pki_int/issue/example-dot-com" "common_name=test.example.com" }}
+{{ .Data.issuing_ca }}{{ end }}
+
ca.tpl
{{- /* /tmp/cert.tpl */ -}}
+{{ with secret "pki_int/issue/example-dot-com" "common_name=test.example.com" }}
+{{ .Data.certificate }}{{ end }}
+
Key.tpl
{{- /* /tmp/key.tpl */ -}}
+{{ with secret "pki_int/issue/example-dot-com" "common_name=test.example.com" }}
+{{ .Data.private_key }}{{ end }}
+
script (e.g. start.sh)
vault agent -config=/root/vault_agent/vault_agent.hcl -log-level=debug
+
service (e.g. vault-agent.service)
[Unit]
+Description=Vault Service Discovery Agent
+Documentation=https://www.vaultproject.io/
+After=network-online.target
+Wants=network-online.target
+
+[Service]
+Type=simple
+User=vault
+Group=vault
+ExecStart=/usr/local/bin/vault agent -config=/root/vault_agent/vault_agent.hcl
+
+ExecReload=/bin/kill -HUP $MAINPID
+KillSignal=SIGINT
+TimeoutStopSec=5
+Restart=on-failure
+SyslogIdentifier=vault
+
+[Install]
+WantedBy=multi-user.target
+
pki revoke
$ vault write pki_int/revoke serial_number="56:ac:c0:f3:b4:1e:87:69:ec:dd:7d:27:54:f6:1c:14:91:3d:11:2d"
+Key Value
+--- -----
+revocation_time 1611557908
+revocation_time_rfc3339 2021-01-25T06:58:28.592511981Z
+
pki rotate
$ vault read pki_int/crl/rotate
+Key Value
+--- -----
+success true
+
테스트 대상 시스템에 CA, Intermediated 인증서를 신뢰할 수 있는 인증서로 등록
[vault_agent.hcl]
pid_file = "/root/vault_agent/pidfile"
+
+auto_auth {
+ method {
+ type = "approle"
+ config = {
+ role_id_file_path = "/root/vault_agent/roleid"
+ secret_id_file_path = "/root/vault_agent/secretid"
+ }
+ }
+
+ sink {
+ type = "file"
+ config = {
+ path = "/tmp/vault_agent"
+ }
+ }
+}
+
+vault {
+ address = "http://172.28.128.11:8200"
+}
+
+template {
+ source = "/root/vault_agent/cert.tpl"
+ destination = "/root/cert/test.cert.pem"
+ perms = "0600"
+}
+
+template {
+ source = "/root/vault_agent/key.tpl"
+ destination = "/root/cert/test.key.pem"
+ perms = "0600"
+}
+
[template - pem_bundle]
cert.tpl
{{- /* /tmp/cert.tpl */ -}}
+{{ with secret "pki_int/issue/example-dot-com" "common_name=test.example.com" "ttl=2m"}}
+{{ .Data.certificate }}
+{{ .Data.issuing_ca }}{{ end }}
+
Key.tpl
{{- /* /tmp/key.tpl */ -}}
+{{ with secret "pki_int/issue/example-dot-com" "common_name=test.example.com" "ttl=2m"}}
+{{ .Data.private_key }}{{ end }}
+
[/etc/nginx/sites-enabled/default]
rotation 되면 systemctl reload nginx
server {
+ listen 80;
+ server_name text.example.com;
+ return 301 HTTPS://$server_name$request_uri;
+}
+
+server {
+ listen 443 ssl default_server;
+ listen [::]:443 ssl default_server;
+
+ ssl on;
+ server_name test.example.com;
+ ssl_certificate /root/cert/test.cert.pem;
+ ssl_certificate_key /root/cert/test.key.pem;
+
+ root /var/www/html;
+
+ # Add index.php to the list if you are using PHP
+ index index.html index.htm index.nginx-debian.html;
+
+ server_name _;
+
+ location / {
+ # First attempt to serve request as file, then
+ # as directory, then fall back to displaying a 404.
+ try_files $uri $uri/ =404;
+ }
+}
+
시크릿 엔진 활성화
$ vault secrets enable -path ssh ssh
+
롤 생성
$ vault write ssh/roles/otp_key_role \
+ key_type=otp \
+ default_user=test \
+ allowed_users=test \
+ key_bits=2048 \
+ cidr_list=172.28.128.0/24
+Success! Data written to: ssh/roles/otp_key_role
+
e.g. cidr_list=127.0.0.1/32,172.28.128.1/32 or 0.0.0.0/0
$ sudo adduser test
+
vault_addr = "http://172.28.128.21:8200"
+ssh_mount_point = "ssh"
+namespace = ""
+tls_skip_verify = true
+allowed_roles = "*"
+allowed_cidr_list = "0.0.0.0/0"
+
https://releases.hashicorp.com/vault-ssh-helper/
tls를 사용하지 않는 경우.-dev
아래와 같이 검증
vault-ssh-helper -verify-only -config=config.hcl -dev
+
/etc/pam.d/sshd
파일의 @include common-auth
부분을 다음과 같이 변경 추가
#@include common-auth
+auth requisite pam_exec.so quiet expose_authtok log=/tmp/vaultssh.log /usr/local/bin/vault-ssh-helper -config=/etc/vault-ssh-helper.d/config.hcl -dev
+auth optional pam_unix.so not_set_pass use_first_pass nodelay
+
/etc/pam.d/common-auth
파일의 auth [success=1 dufault=ignore]
아래 2줄 추가
# here are the per-package modules (the "Primary" block)
+auth [success=1 default=ignore] pam_unix.so nullok_secure
+auth [success=3 default=ignore] pam_exec.so quiet expose_authtok log=/tmp/vaultssh.log /usr/local/bin/vault-ssh-helper -config=/etc/vault-ssh-helper.d/config.hcl -dev
+auth [success=2 default=ignore] pam_unix.so not_set_pass use_first_pass nodelay
+# here's the fallback if no module succeeds
+auth requisite pam_deny.so
+# prime the stack with a positive return value if there isn't one already;
+# this avoids us returning an error just because nothing sets a success code
+# since the modules above will each just jump around
+auth required pam_permit.so
+# and here are more per-package modules (the "Additional" block)
+auth optional pam_cap.so
+# end of pam-auth-update config
+
/etc/ssh/sshd_config
파일의 ChallengeResponseAuthentication
부분을 수정
이미 있는 옵션은 값 수정, 없는 옵션은 추가
ChallengeResponseAuthentication yes
+UsePAM yes
+PasswordAuthentication no
+
$ systemctl restart ssh
+
otp 발급
$ vault write ssh/creds/otp_key_role ip=172.28.128.31
+Key Value
+--- -----
+lease_id ssh/creds/otp_key_role/r2SFjhwt3brVT0msL1KEq2Dv
+lease_duration 768h
+lease_renewable false
+ip 172.28.128.31
+key f8a32d6c-beec-383c-62d6-3718b367f88d
+key_type otp
+port 22
+username test
+
Password: 에 앞서 요청한 롤의 credential 값의
key
를 넣어준다.
$ ssh test@172.28.128.31
+Password:
+
Vault로 해당 ssh otp에 권한이 있는 사용자인 경우
sshpass
가 설치되어있으면 자동 입력
$ vault ssh -role otp_key_role -mode otp test@172.28.128.31
+or
+$ vault ssh -role otp_key_role -mode otp -strict-host-key-checking=no test@172.28.128.31
+
시크릿 엔진 활성화
$ vault secrets enable -path ssh ssh
+
롤 생성
$ vault write ssh/roles/otp_key_role \
+ key_type=otp \
+ default_user=test \
+ allowed_users=test \
+ key_bits=2048 \
+ cidr_list=172.28.128.0/24
+Success! Data written to: ssh/roles/otp_key_role
+
e.g. cidr_list=127.0.0.1/32,172.28.128.1/32 or 0.0.0.0/0
접속할 사용자 생성
$ sudo adduser test
+
vault-ssh-helper 구성 <config.hcl>
vault_addr = "http://172.28.128.21:8200"
+ssh_mount_point = "ssh"
+namespace = ""
+tls_skip_verify = true
+allowed_roles = "*"
+allowed_cidr_list = "0.0.0.0/0"
+
vault-ssh-helper 다운로드
vault-ssh-helper verify
tls를 사용하지 않는 경우.
-dev
vault-ssh-helper -verify-only -config=config.hcl -dev
+
Pam 설정
/etc/pam.d/sshd
파일의 @include common-auth
부분을 다음과 같이 변경 추가
#%PAM-1.0
+auth required pam_sepermit.so
+#auth substack password-auth # COMMENT OUT FOR SSH-HELPER
+auth include postlogin
+auth requisite pam_exec.so quiet expose_authtok log=/var/log/vaultssh.log /usr/local/bin/vault-ssh-helper -config=/etc/vault-ssh-helper.d/config.hcl -dev
+auth optional pam_unix.so not_set_pass use_first_pass nodelay
+# Used with polkit to reauthorize users in remote sessions
+-auth optional pam_reauthorize.so prepare
+account required pam_nologin.so
+account include password-auth
+#password include password-auth # COMMENT OUT FOR SSH-HELPER
+# pam_selinux.so close should be the first session rule
+session required pam_selinux.so close
+session required pam_loginuid.so
+# pam_selinux.so open should only be followed by sessions to be executed in the user context
+session required pam_selinux.so open env_params
+session required pam_namespace.so
+session optional pam_keyinit.so force revoke
+session include password-auth
+session include postlogin
+# Used with polkit to reauthorize users in remote sessions
+-session optional pam_reauthorize.so prepare
+
ssh 설정
/etc/ssh/sshd_config
파일의 다음 항목을 수정
이미 있는 옵션은 값 수정, 없는 옵션은 추가
ChallengeResponseAuthentication yes
+UsePAM yes
+PasswordAuthentication no
+
ssh 서비스 재시작
$ systemctl restart sshd
+
otp 발급
$ vault write ssh/creds/otp_key_role ip=172.28.128.31
+Key Value
+--- -----
+lease_id ssh/creds/otp_key_role/r2SFjhwt3brVT0msL1KEq2Dv
+lease_duration 768h
+lease_renewable false
+ip 172.28.128.31
+key f8a32d6c-beec-383c-62d6-3718b367f88d
+key_type otp
+port 22
+username test
+
접속 방법 1. ssh
Password: 에 앞서 요청한 롤의 credential 값의
key
를 넣어준다.
$ ssh test@172.28.128.31
+Password:
+
접속 방법 2. vault ssh
Vault로 해당 ssh otp에 권한이 있는 사용자인 경우
sshpass
가 설치되어있으면 자동 입력
$ vault ssh -role otp_key_role -mode otp test@172.28.128.31
+or
+$ vault ssh -role otp_key_role -mode otp -strict-host-key-checking=no test@172.28.128.31
+
시크릿 엔진 활성화
$ vault secrets enable -path=ssh-client-signer ssh
+
$ vault write ssh-client-signer/config/ca generate_signing_key=true
+Key Value
+--- -----
+public_key ...
+
keypair가 이미 있는 경우
$ vault write ssh-client-signer/config/ca \
+ private_key="..." \
+ public_key="..."
+
$ curl -o /etc/ssh/trusted-user-ca-keys.pem http://127.0.0.1:8200/v1/ssh-client-signer/public_key
+
$ vault read -field=public_key ssh-client-signer/config/ca > /tmp/trusted-user-ca-keys.pem
+$ /tmp/trusted-user-ca-keys.pem
+/etc/ssh/trusted-user-ca-keys.pem로 복사
+
/etc/ssh/sshd_config
파일의 TrustedUserCAKeys
부분을 수정
이미 있는 옵션은 값 수정, 없는 옵션은 추가
TrustedUserCAKeys /etc/ssh/trusted-user-ca-keys.pem
+
ssh 서비스 재시작
$ systemctl restart sshd
+
TTL 2분
$ vault write ssh-client-signer/roles/my-role - <<EOF
+{
+ "allow_user_certificates": true,
+ "allowed_users": "*",
+ "allowed_extensions": "permit-pty,permit-port-forwarding",
+ "default_extensions": [
+ {
+ "permit-pty": ""
+ }
+ ],
+ "key_type": "ca",
+ "default_user": "test",
+ "ttl": "0m20s"
+}
+EOF
+
클라이언트에서 SSH에서 사용할 Keypair를 생성
$ ssh-keygen -t rsa -C "test@rocky"
+Generating public/private rsa key pair.
+Enter file in which to save the key (/Users/gs/.ssh/id_rsa): /Users/gs/.ssh/vault_rsa
+
-C
: 코맨트 옵션Vault에 생성한 키 중 공개키 (.pub)에 대한 서명 요청
$ vault write ssh-client-signer/sign/my-role \
+ public_key=@$HOME/.ssh/vault_rsa.pub
+
+Key Value
+--- -----
+serial_number c73f26d2340276aa
+signed_key ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1...
+
위와 같은 방식으로 생성되는 추가 public key인 signed_key를 저장하고자 한다면 다음과 같은 방식으로 가능
$ vault write -field=signed_key ssh-client-signer/sign/my-role public_key=@$HOME/.ssh/vault_rsa.pub > /tmp/signed-cert.pub
+
+$ vault write -field=signed_key ssh-client-signer/sign/my-role \
+ public_key=@$HOME/.ssh/vault_rsa.pub > $HOME/.ssh/vault_rsa-cert.pub
+
접속 하기
$ ssh -i /tmp/signed-cert.pub -i ~/.ssh/vault_rsa test@172.28.128.61
+$ ssh -i ~/.ssh/id_rsa test@172.28.128.61
+
Transform secrets 엔진은 제공된 입력 값에 대해 안전한 데이터 변환 및 토큰화를 처리합니다. 변환 방법은 FF3-1 을 통한 형태 보존 암호화(FPE) 와 같은 NIST 검증된 암호화 표준을 포함 할 수 있지만 마스킹과 같은 다른 수단을 통한 데이터의 익명 변환일 수도 있습니다.
아래 영상에서는 UI로 진행하는 방식을 설명합니다.
팁
Enterprise 라이선스가 필요하기 때문에 라이선스가 필요한 경우 Trial 을 발급 받을 수 있습니다.
: https://www.hashicorp.com/products/vault/trial
Transform은 엔터프라이즈 기능으로, 테스트를 위해서는 엔터프라이즈 바이너리가 필요합니다. 디렉토리 뒤에 +ent
로 표기되어있습니다.
-dev
옵션을 추가하여 테스트를 위한 개발모드 실행을 위해서는 라이선스파일에 대한 경로 설정이 필요합니다.
export VAULT_LICENSE_PATH=<license_file_path>
+vault server -dev
+
CLI 테스트 시 필요한 환경변수는 다음과 같습니다.
export VAULT_SKIP_VERIFY=True
+export VAULT_ADDR='http://127.0.0.1:8200'
+export VAULT_TOKEN=<mytoken>
+
transform
시크릿 엔진을 활성화 합니다.vault secrets enable transform
+
vault write transform/template/phone-number-tmpl \
+ type=regex \
+ pattern='\d{3}-(\d{4})-(\d{4})' \
+ alphabet=builtin/numeric
+
()
에 해당하는 위치가 암호화 됩니다.vault write transform/transformations/fpe/phone-number \
+ template="phone-number-tmpl" \
+ tweak_source=internal \
+ allowed_roles=customer
+
vault write transform/role/customer transformations=phone-number
+
암호화
vault write transform/encode/customer value=010-1234-5678 \
+ transformation=phone-number
+
Key Value
+--- -----
+encoded_value 010-7494-8066
+
복호화
vault write transform/decode/customer value=010-7494-8066 \
+ transformation=phone-number
+
Key Value
+--- -----
+decoded_value 010-1234-5678
+
vault secrets enable transform
+
+vault write transform/alphabet/hangul alphabet="가각간갇갈감갑갓강개객갠갣갤갬갭갯갱갸갹갼갿걀걈걉걋걍걔걕걘걛걜걤걥걧걩거걱건걷걸검겁것겅게겍겐겓겔겜겝겟겡겨격견겯결겸겹겻경계곅곈곋곌곔곕곗곙고곡곤곧골곰곱곳공과곽관괃괄괌괍괏광괘괙괜괟괠괨괩괫괭괴괵괸괻괼굄굅굇굉교굑굔굗굘굠굡굣굥구국군굳굴굼굽굿궁궈궉권궏궐궘궙궛궝궤궥궨궫궬궴궵궷궹귀귁귄귇귈귐귑귓귕규귝균귣귤귬귭귯귱그극근귿글금급긋긍긔긕긘긛긜긤긥긧긩기긱긴긷길김깁깃깅까깍깐깓깔깜깝깟깡깨깩깬깯깰깸깹깻깽꺄꺅꺈꺋꺌꺔꺕꺗꺙꺠꺡꺤꺧꺨꺰꺱꺳꺵꺼꺽껀껃껄껌껍껏껑께껙껜껟껠껨껩껫껭껴껵껸껻껼꼄꼅꼇꼉꼐꼑꼔꼗꼘꼠꼡꼣꼥꼬꼭꼰꼳꼴꼼꼽꼿꽁꽈꽉꽌꽏꽐꽘꽙꽛꽝꽤꽥꽨꽫꽬꽴꽵꽷꽹꾀꾁꾄꾇꾈꾐꾑꾓꾕꾜꾝꾠꾣꾤꾬꾭꾯꾱꾸꾹꾼꾿꿀꿈꿉꿋꿍꿔꿕꿘꿛꿜꿤꿥꿧꿩꿰꿱꿴꿷꿸뀀뀁뀃뀅뀌뀍뀐뀓뀔뀜뀝뀟뀡뀨뀩뀬뀯뀰뀸뀹뀻뀽끄끅끈끋끌끔끕끗끙끠끡끤끧끨끰끱끳끵끼끽낀낃낄낌낍낏낑나낙난낟날남납낫낭내낵낸낻낼냄냅냇냉냐냑냔냗냘냠냡냣냥냬냭냰냳냴냼냽냿넁너넉넌넏널넘넙넛넝네넥넨넫넬넴넵넷넹녀녁년녇녈념녑녓녕녜녝녠녣녤녬녭녯녱노녹논녿놀놈놉놋농놔놕놘놛놜놤놥놧놩놰놱놴놷놸뇀뇁뇃뇅뇌뇍뇐뇓뇔뇜뇝뇟뇡뇨뇩뇬뇯뇰뇸뇹뇻뇽누눅눈눋눌눔눕눗눙눠눡눤눧눨눰눱눳눵눼눽뉀뉃뉄뉌뉍뉏뉑뉘뉙뉜뉟뉠뉨뉩뉫뉭뉴뉵뉸뉻뉼늄늅늇늉느늑는늗늘늠늡늣능늬늭늰늳늴늼늽늿닁니닉닌닏닐님닙닛닝다닥단닫달담답닷당대댁댄댇댈댐댑댓댕댜댝댠댣댤댬댭댯댱댸댹댼댿덀덈덉덋덍더덕던덛덜덤덥덧덩데덱덴덷델뎀뎁뎃뎅뎌뎍뎐뎓뎔뎜뎝뎟뎡뎨뎩뎬뎯뎰뎸뎹뎻뎽도독돈돋돌돔돕돗동돠돡돤돧돨돰돱돳돵돼돽됀됃됄됌됍됏됑되됙된됟될됨됩됫됭됴됵됸됻됼둄둅둇둉두둑둔둗둘둠둡둣둥둬둭둰둳둴둼둽둿뒁뒈뒉뒌뒏뒐뒘뒙뒛뒝뒤뒥뒨뒫뒬뒴뒵뒷뒹듀듁듄듇듈듐듑듓듕드득든듣들듬듭듯등듸듹듼듿딀딈딉딋딍디딕딘딛딜딤딥딧딩따딱딴딷딸땀땁땃땅때땍땐땓땔땜땝땟땡땨땩땬땯땰땸땹땻땽떄떅떈떋떌떔떕떗떙떠떡떤떧떨떰떱떳떵떼떽뗀뗃뗄뗌뗍뗏뗑뗘뗙뗜뗟뗠뗨뗩뗫뗭뗴뗵뗸뗻뗼똄똅똇똉또똑똔똗똘똠똡똣똥똬똭똰똳똴똼똽똿뙁뙈뙉뙌뙏뙐뙘뙙뙛뙝뙤뙥뙨뙫뙬뙴뙵뙷뙹뚀뚁뚄뚇뚈뚐뚑뚓뚕뚜뚝뚠뚣뚤뚬뚭뚯뚱뚸뚹뚼뚿뛀뛈뛉뛋뛍뛔뛕뛘뛛뛜뛤뛥뛧뛩뛰뛱뛴뛷뛸뜀뜁뜃뜅뜌뜍뜐뜓뜔뜜뜝뜟뜡뜨뜩뜬뜯뜰뜸뜹뜻뜽띄띅띈띋띌띔띕띗띙띠띡띤띧띨띰띱띳띵라락란랃랄람랍랏랑래랙랜랟랠램랩랫랭랴략랸랻랼럄럅럇량럐럑럔럗럘럠럡럣럥러럭런럳럴럼럽럿렁레렉렌렏렐렘렙렛렝려력련렫렬렴렵렷령례롁롄롇롈롐롑롓롕로록론롣롤롬롭롯롱롸롹롼롿뢀뢈뢉뢋뢍뢔뢕뢘뢛뢜뢤뢥뢧뢩뢰뢱뢴뢷뢸룀룁룃룅료룍룐룓룔룜룝룟룡루룩룬룯룰룸룹룻룽뤄뤅뤈뤋뤌뤔뤕뤗뤙뤠뤡뤤뤧뤨뤰뤱뤳뤵뤼뤽륀륃륄륌륍륏륑류륙륜륟률륨륩륫륭르륵른륻를름릅릇릉릐릑릔릗릘릠릡릣릥리릭린릳릴림립릿링마막만맏말맘맙맛망매맥맨맫맬맴맵맷맹먀먁먄먇먈먐먑먓먕먜먝먠먣먤먬먭먯먱머먹먼먿멀멈멉멋멍메멕멘멛멜멤멥멧멩며멱면멷멸몀몁몃명몌몍몐몓몔몜몝몟몡모목몬몯몰몸몹못몽뫄뫅뫈뫋뫌뫔뫕뫗뫙뫠뫡뫤뫧뫨뫰뫱뫳뫵뫼뫽묀묃묄묌묍묏묑묘묙묜묟묠묨묩묫묭무묵문묻물뭄뭅뭇뭉뭐뭑뭔뭗뭘뭠뭡뭣뭥뭬뭭뭰뭳뭴뭼뭽뭿뮁뮈뮉뮌뮏뮐뮘뮙뮛뮝뮤뮥뮨뮫뮬뮴뮵뮷뮹므믁믄믇믈믐믑믓믕믜믝믠믣믤믬믭믯믱미믹민믿밀밈밉밋밍바박반받발밤밥밧방배백밴밷밸뱀뱁뱃뱅뱌뱍뱐뱓뱔뱜뱝뱟뱡뱨뱩뱬뱯뱰뱸뱹뱻뱽버벅번벋벌범법벗벙베벡벤벧벨벰벱벳벵벼벽변볃별볌볍볏병볘볙볜볟볠볨볩볫볭보복본볻볼봄봅봇봉봐봑봔봗봘봠봡봣봥봬봭봰봳봴봼봽봿뵁뵈뵉뵌뵏뵐뵘뵙뵛뵝뵤뵥뵨뵫뵬뵴뵵뵷뵹부북분붇불붐붑붓붕붜붝붠붣붤붬붭붯붱붸붹붼붿뷀뷈뷉뷋뷍뷔뷕뷘뷛뷜뷤뷥뷧뷩뷰뷱뷴뷷뷸븀븁븃븅브븍븐븓블븜븝븟븡븨븩븬븯븰븸븹븻븽비빅빈빋빌빔빕빗빙빠빡빤빧빨빰빱빳빵빼빽뺀뺃뺄뺌뺍뺏뺑뺘뺙뺜뺟뺠뺨뺩뺫뺭뺴뺵뺸뺻뺼뻄뻅뻇뻉뻐뻑뻔뻗뻘뻠뻡뻣뻥뻬뻭뻰뻳뻴뻼뻽뻿뼁뼈뼉뼌뼏뼐뼘뼙뼛뼝뼤뼥뼨뼫뼬뼴뼵뼷뼹뽀뽁뽄뽇뽈뽐뽑뽓뽕뽜뽝뽠뽣뽤뽬뽭뽯뽱뽸뽹뽼뽿뾀뾈뾉뾋뾍뾔뾕뾘뾛뾜뾤뾥뾧뾩뾰뾱뾴뾷뾸뿀뿁뿃뿅뿌뿍뿐뿓뿔뿜뿝뿟뿡뿨뿩뿬뿯뿰뿸뿹뿻뿽쀄쀅쀈쀋쀌쀔쀕쀗쀙쀠쀡쀤쀧쀨쀰쀱쀳쀵쀼쀽쁀쁃쁄쁌쁍쁏쁑쁘쁙쁜쁟쁠쁨쁩쁫쁭쁴쁵쁸쁻쁼삄삅삇삉삐삑삔삗삘삠삡삣삥사삭산삳살삼삽삿상새색샌샏샐샘샙샛생샤샥샨샫샬샴샵샷샹섀섁섄섇섈섐섑섓섕서석선섣설섬섭섯성세섹센섿셀셈셉셋셍셔셕션셛셜셤셥셧셩셰셱셴셷셸솀솁솃솅소속손솓솔솜솝솟송솨솩솬솯솰솸솹솻솽쇄쇅쇈쇋쇌쇔쇕쇗쇙쇠쇡쇤쇧쇨쇰쇱쇳쇵쇼쇽숀숃숄숌숍숏숑수숙순숟술숨숩숫숭숴숵숸숻숼쉄쉅쉇쉉쉐쉑쉔쉗쉘쉠쉡쉣쉥쉬쉭쉰쉳쉴쉼쉽쉿슁슈슉슌슏슐슘슙슛슝스슥슨슫슬슴습슷승싀싁싄싇싈싐싑싓싕시식신싣실심십싯싱싸싹싼싿쌀쌈쌉쌋쌍쌔쌕쌘쌛쌜쌤쌥쌧쌩쌰쌱쌴쌷쌸썀썁썃썅썌썍썐썓썔썜썝썟썡써썩썬썯썰썸썹썻썽쎄쎅쎈쎋쎌쎔쎕쎗쎙쎠쎡쎤쎧쎨쎰쎱쎳쎵쎼쎽쏀쏃쏄쏌쏍쏏쏑쏘쏙쏜쏟쏠쏨쏩쏫쏭쏴쏵쏸쏻쏼쐄쐅쐇쐉쐐쐑쐔쐗쐘쐠쐡쐣쐥쐬쐭쐰쐳쐴쐼쐽쐿쑁쑈쑉쑌쑏쑐쑘쑙쑛쑝쑤쑥쑨쑫쑬쑴쑵쑷쑹쒀쒁쒄쒇쒈쒐쒑쒓쒕쒜쒝쒠쒣쒤쒬쒭쒯쒱쒸쒹쒼쒿쓀쓈쓉쓋쓍쓔쓕쓘쓛쓜쓤쓥쓧쓩쓰쓱쓴쓷쓸씀씁씃씅씌씍씐씓씔씜씝씟씡씨씩씬씯씰씸씹씻씽아악안앋알암압앗앙애액앤앧앨앰앱앳앵야약얀얃얄얌얍얏양얘얙얜얟얠얨얩얫얭어억언얻얼엄업엇엉에엑엔엗엘엠엡엣엥여역연엳열염엽엿영예옉옌옏옐옘옙옛옝오옥온옫올옴옵옷옹와왁완왇왈왐왑왓왕왜왝왠왣왤왬왭왯왱외왹왼왿욀욈욉욋욍요욕욘욛욜욤욥욧용우욱운욷울움웁웃웅워웍원웓월웜웝웟웡웨웩웬웯웰웸웹웻웽위윅윈윋윌윔윕윗윙유육윤윧율윰윱윳융으윽은읃을음읍읏응의읙읜읟읠읨읩읫읭이익인읻일임입잇잉자작잔잗잘잠잡잣장재잭잰잳잴잼잽잿쟁쟈쟉쟌쟏쟐쟘쟙쟛쟝쟤쟥쟨쟫쟬쟴쟵쟷쟹저적전젇절점접젓정제젝젠젣젤젬젭젯젱져젹젼젿졀졈졉졋졍졔졕졘졛졜졤졥졧졩조족존졷졸좀좁좃종좌좍좐좓좔좜좝좟좡좨좩좬좯좰좸좹좻좽죄죅죈죋죌죔죕죗죙죠죡죤죧죨죰죱죳죵주죽준줃줄줌줍줏중줘줙줜줟줠줨줩줫줭줴줵줸줻줼쥄쥅쥇쥉쥐쥑쥔쥗쥘쥠쥡쥣쥥쥬쥭쥰쥳쥴쥼쥽쥿즁즈즉즌즏즐즘즙즛증즤즥즨즫즬즴즵즷즹지직진짇질짐집짓징짜짝짠짣짤짬짭짯짱째짹짼짿쨀쨈쨉쨋쨍쨔쨕쨘쨛쨜쨤쨥쨧쨩쨰쨱쨴쨷쨸쩀쩁쩃쩅쩌쩍쩐쩓쩔쩜쩝쩟쩡쩨쩩쩬쩯쩰쩸쩹쩻쩽쪄쪅쪈쪋쪌쪔쪕쪗쪙쪠쪡쪤쪧쪨쪰쪱쪳쪵쪼쪽쫀쫃쫄쫌쫍쫏쫑쫘쫙쫜쫟쫠쫨쫩쫫쫭쫴쫵쫸쫻쫼쬄쬅쬇쬉쬐쬑쬔쬗쬘쬠쬡쬣쬥쬬쬭쬰쬳쬴쬼쬽쬿쭁쭈쭉쭌쭏쭐쭘쭙쭛쭝쭤쭥쭨쭫쭬쭴쭵쭷쭹쮀쮁쮄쮇쮈쮐쮑쮓쮕쮜쮝쮠쮣쮤쮬쮭쮯쮱쮸쮹쮼쮿쯀쯈쯉쯋쯍쯔쯕쯘쯛쯜쯤쯥쯧쯩쯰쯱쯴쯷쯸찀찁찃찅찌찍찐찓찔찜찝찟찡차착찬찯찰참찹찻창채책챈챋챌챔챕챗챙챠챡챤챧챨챰챱챳챵챼챽첀첃첄첌첍첏첑처척천첟철첨첩첫청체첵첸첻첼쳄쳅쳇쳉쳐쳑쳔쳗쳘쳠쳡쳣쳥쳬쳭쳰쳳쳴쳼쳽쳿촁초촉촌촏촐촘촙촛총촤촥촨촫촬촴촵촷촹쵀쵁쵄쵇쵈쵐쵑쵓쵕최쵝쵠쵣쵤쵬쵭쵯쵱쵸쵹쵼쵿춀춈춉춋춍추축춘춛출춤춥춧충춰춱춴춷춸췀췁췃췅췌췍췐췓췔췜췝췟췡취췩췬췯췰췸췹췻췽츄츅츈츋츌츔츕츗츙츠측츤츧츨츰츱츳층츼츽칀칃칄칌칍칏칑치칙친칟칠침칩칫칭카칵칸칻칼캄캅캇캉캐캑캔캗캘캠캡캣캥캬캭캰캳캴캼캽캿컁컈컉컌컏컐컘컙컛컝커컥컨컫컬컴컵컷컹케켁켄켇켈켐켑켓켕켜켝켠켣켤켬켭켯켱켸켹켼켿콀콈콉콋콍코콕콘콛콜콤콥콧콩콰콱콴콷콸쾀쾁쾃쾅쾌쾍쾐쾓쾔쾜쾝쾟쾡쾨쾩쾬쾯쾰쾸쾹쾻쾽쿄쿅쿈쿋쿌쿔쿕쿗쿙쿠쿡쿤쿧쿨쿰쿱쿳쿵쿼쿽퀀퀃퀄퀌퀍퀏퀑퀘퀙퀜퀟퀠퀨퀩퀫퀭퀴퀵퀸퀻퀼큄큅큇큉큐큑큔큗큘큠큡큣큥크큭큰큳클큼큽큿킁킈킉킌킏킐킘킙킛킝키킥킨킫킬킴킵킷킹타탁탄탇탈탐탑탓탕태택탠탣탤탬탭탯탱탸탹탼탿턀턈턉턋턍턔턕턘턛턜턤턥턧턩터턱턴턷털텀텁텃텅테텍텐텓텔템텝텟텡텨텩텬텯텰텸텹텻텽톄톅톈톋톌톔톕톗톙토톡톤톧톨톰톱톳통톼톽퇀퇃퇄퇌퇍퇏퇑퇘퇙퇜퇟퇠퇨퇩퇫퇭퇴퇵퇸퇻퇼툄툅툇툉툐툑툔툗툘툠툡툣툥투툭툰툳툴툼툽툿퉁퉈퉉퉌퉏퉐퉘퉙퉛퉝퉤퉥퉨퉫퉬퉴퉵퉷퉹튀튁튄튇튈튐튑튓튕튜튝튠튣튤튬튭튯튱트특튼튿틀틈틉틋틍틔틕틘틛틜틤틥틧틩티틱틴틷틸팀팁팃팅파팍판팓팔팜팝팟팡패팩팬팯팰팸팹팻팽퍄퍅퍈퍋퍌퍔퍕퍗퍙퍠퍡퍤퍧퍨퍰퍱퍳퍵퍼퍽펀펃펄펌펍펏펑페펙펜펟펠펨펩펫펭펴펵편펻펼폄폅폇평폐폑폔폗폘폠폡폣폥포폭폰폳폴폼폽폿퐁퐈퐉퐌퐏퐐퐘퐙퐛퐝퐤퐥퐨퐫퐬퐴퐵퐷퐹푀푁푄푇푈푐푑푓푕표푝푠푣푤푬푭푯푱푸푹푼푿풀품풉풋풍풔풕풘풛풜풤풥풧풩풰풱풴풷풸퓀퓁퓃퓅퓌퓍퓐퓓퓔퓜퓝퓟퓡퓨퓩퓬퓯퓰퓸퓹퓻퓽프픅픈픋플픔픕픗픙픠픡픤픧픨픰픱픳픵피픽핀핃필핌핍핏핑하학한핟할함합핫항해핵핸핻핼햄햅햇행햐햑햔햗햘햠햡햣향햬햭햰햳햴햼햽햿헁허헉헌헏헐험헙헛헝헤헥헨헫헬헴헵헷헹혀혁현혇혈혐협혓형혜혝혠혣혤혬혭혯혱호혹혼혿홀홈홉홋홍화확환홛활홤홥홧황홰홱홴홷홸횀횁횃횅회획횐횓횔횜횝횟횡효횩횬횯횰횸횹횻횽후훅훈훋훌훔훕훗훙훠훡훤훧훨훰훱훳훵훼훽휀휃휄휌휍휏휑휘휙휜휟휠휨휩휫휭휴휵휸휻휼흄흅흇흉흐흑흔흗흘흠흡흣흥희흭흰흳흴흼흽흿힁히힉힌힏힐힘힙힛힝"
+
+vault write transform/template/kr-name-tmpl \
+ type=regex \
+ pattern='([가-힣]{2,4})' \
+ alphabet=hangul
+
+vault write transform/transformations/fpe/kr-name \
+template="kr-name-tmpl" \
+tweak_source=internal \
+allowed_roles="*"
+
+vault write transform/role/customer transformations=kr-name
+
+vault write transform/encode/customer value="한볼트" \
+ transformation=kr-name
+
+Key Value
+--- -----
+encoded_value 붏숁삂
+
+vault write transform/decode/customer value="붏숁삂" \
+ transformation=kr-name
+
+Key Value
+--- -----
+decoded_value 한볼트
+
키 가져오기(Import) 기능은 HSM, 사용자 정의 키, 기타 외부 시스템에서 기존 키를 가져와야 하는 경우를 지원한다. 공개키(Public Key)만을 가져올 수도 있다.
links
transit import
와 transit import-version
은 BYOK 메커니즘을 통해 사용자가 생성한 키를 Transit 엔진에서 사용할 키로 가져온다.
transit import
: 키를 새 키로 가져오며 이미 존재하는 경우 실패transit import-version
: 기존 키를 새 버전의 키로 업데이트openssl
을 이용하여 RSA 키를 생성해보고, 데이터를 암호화 및 복호화
먼저 openssl
을 이용하여 RSA 키쌍을 생성한다. 이 예에서는 2048비트 키를 생성한다.
# Private 키
+openssl genrsa -out private_key.pem 2048
+# Public 키
+openssl rsa -pubout -in private_key.pem -out public_key.pem
+
private_key.pem
에 개인 키가 저장되고 public_key.pem
에 공개 키가 저장된다.
생성된 PEM 예제
-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCxAqZGiw7/Et52
+TRHgLFVYi3HbnMB5m/ZBMu/CZxk6H5zFrCIXcBFh+K58P/rMydFh2rveTd6CT1+s
+zrphe1MPS9mRjvvgy3Bk1XEEUBYOlmIk/eD3lLJEoTjY1E5bpuxirTgV7rR47XEy
+ZsdT6g8Z6s6/M4bjCnJ4ZuBu1VZ3e/5pHrYFSEudy7Xag1Uby1T0Txr/OPPPjVYO
+A9jgGAO+0wr4elPHyYua1IDnQGfOVuGMKKei+PybKdUvErtp6Z5PJxl7Ylj0Uq8V
+ym2iXyF7deryXj4vwDZg4UdbX6TcNVSCSM5rvKtAf3S7AsSwflG9WO5Kt1t7QGy2
+EWVLTrnNAgMBAAECggEASfZJdAB2663+tn/NkFX182GQ2arN4gKBCw01kY1yxQ6g
+exhJxnFVUhKPFevF5/wMOU8kYOc4qkpwN8zJpCHbuvB+oIuWQ3++HuPwrVSpYr8D
+k2FhtxGyy2pyTmentjQxYanvXXq4fi74tY6siyup07KBYPMu0X90BUs3TBhoYNQE
+KlcZXAR20Y+8NCsKa3QmX9yXUOmDUz5i0zWo7Ojwlig96GpJXq8au3NJcarzFZsw
+YFkGelNIMCDcH05ao8ujOoKecmusMEOGoue1DOzduFAvFRGoo7Cx1C+O2ORR0uwC
+jyO2H7qrIckoBlnjSzJ5GOY2UNyLAGs5cNEy6Na9IQKBgQDmoSCfx/DSC00lFY7H
+Z0fsJrQWzhFnj5hyP3bti4GdtCtYxf1jgM+ZPt8SNU1fqWl2+JbDMXM0C65z39bA
+YcHYeWYHVXGMVU/6vDyeD3l7ohuNi7GwvnejZmN0QWoIpKQ9OeVZWQ243bKg7UTR
+SDWrOj30RJoS6CzfoKUIM+yCLwKBgQDEe4dJ452/0RsLFhuICQmP9GSUCHZCnSBv
+RZvWlw6IJ0qL+Ww+fyNKld0BdUFKZsHVamxcEr/e0MMjFYyoq4JGkBJbUZjcg6QI
+bSn4ENKNWEnEfCGeEf2o2IZSdiTGtC8kV1zAgIoy9he/imosMufovjHLXV2QsuDA
+plaQwD1wwwKBgQCf0nkxQPV6GarUUCQpu0D0Pb3/L76P7crPIXvhEhQ4nWqMkmgO
+VG2I3TDpBVchO92CPLL9gX88SfwTAMNpflU/FqHF40hU36oVL+0x+7dMHgLKDEyP
+Fu8BpSq2nb5FTxMh+sUdLcF8ouXu734JKelHR403gXLkN1Ehh8nV7WWwsQKBgEfz
+9NdaQ6q7KOwmbG6k4JuXJD4R2z0JzZbyJt+u8eNqgCJCdSFt7b6iowylpANbHiDJ
+mGUfeKRgTxXKDni2Vj8BA7ftac1XZ/qt/3CYuIKKknkh/C2m6P2sTYRlP5KE6b6l
+P5I/gFypQokiZz9IZSUWgaW3y0vyNdxXDdx0iguBAoGBAOR4r7I8WcOM9i/uTSJm
+oAlI9FqxCmQg6Yly6alMF5jjC2/F2/7byYB1FcZ0EnTomYb4dEePCMron3pBpFIk
+gx2rcjODvK/hU2Uodpy1XF47mx7dGzuTZYLPijRzl3R/5nW05xnDLBRRAQXaBKLj
+CYIKBHwiRAEvHioLiDIostz1
+-----END PRIVATE KEY-----
+
-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsQKmRosO/xLedk0R4CxV
+WItx25zAeZv2QTLvwmcZOh+cxawiF3ARYfiufD/6zMnRYdq73k3egk9frM66YXtT
+D0vZkY774MtwZNVxBFAWDpZiJP3g95SyRKE42NROW6bsYq04Fe60eO1xMmbHU+oP
+GerOvzOG4wpyeGbgbtVWd3v+aR62BUhLncu12oNVG8tU9E8a/zjzz41WDgPY4BgD
+vtMK+HpTx8mLmtSA50BnzlbhjCinovj8mynVLxK7aemeTycZe2JY9FKvFcptol8h
+e3Xq8l4+L8A2YOFHW1+k3DVUgkjOa7yrQH90uwLEsH5RvVjuSrdbe0BsthFlS065
+zQIDAQAB
+-----END PUBLIC KEY-----
+
공개 키(public_key.pem
)를 사용하여 "This is my data"라는 문자열을 암호화한다.
echo "This is my data" | openssl rsautl -encrypt -pubin -inkey public_key.pem -out encrypted_data.bin
+
encrypted_data.bin
�W�U�F�B�� �����F ��u8-�U>���j��;"`Z�+�A��)�6u����9��H�W��t)h��,�m��
+
+ *h��(UL;ZC�l�K��8���*��Y�k?��`�?�
+ �%eܓ�O^
+ K�]���'8�QI�H��2�d���2�Nv$��)F���z���Ձd�B��"�na���x��v/�J-�^�
+ ��ΕJ���̳*
+
+
암호화된 데이터를 복호화하려면 개인 키(private_key.pem
)를 사용해야 한다.
openssl rsautl -decrypt -inkey private_key.pem -in encrypted_data.bin
+
$ openssl rsautl -decrypt -inkey private_key.pem -in encrypted_data.bin
+
+This is my data
+
Import 주의 사항
대칭 키(예: AES 또는 ChaCha20 키)를 래핑할 때는 키의 원시 바이트를 래핑해야 한다. 예를 들어 AES 128비트 키의 경우 16자 길이의 바이트 배열이 되는데, 이 바이트 배열은 Base64나 다른 인코딩 없이 바로 래핑된다.
비대칭 키(예: RSA 또는 ECDSA 키)를 래핑할 때는 이 키의 PKCS8 인코딩된 형식을 원시 DER/바이너리 형식으로 래핑해야 한다.
위 2. OpenSSL을 사용하여 암복호화에서 생성한 개인키를 사용하여 진행한다.
OpenSSL로 생성된 비대칭 키는 DER 형식으로 변경이 필요하다.
openssl pkcs8 -topk8 -nocrypt -inform PEM -outform DER -in private_key.pem -out private_key.der
+
$ cat private_key.der
+
+��0�����F���vM�,UX�qۜ�y��A2��g:�Ŭ"pa�|?����aڻ�MނO_�κa{SKّ����pd�qP�b$���D�8��N[��b�8�x�q2f�S��ο3��
+rxf�n�Vw{�i�HK�˵ڃUT�O�8�ύV����
+�zS�ɋ�Ԁ�@g�V�(����)�/�i�O'{bX�R��m�_!{u��^>/�6`�G[_��5T�H�k��@t�İ~Q�X�J�[{@l�eKN���I�Itv���͐U��a�٪���
+5��r��{I�qUR�����
+ 9O$`�8�Jp7�ɤ!ۺ�~���C���T�b��aa���jrNg��41a��]z�~.�����+�Ӳ�`�.�tK7Lh`�*W\vя�4+
+kt&_ܗP�S>b�5����(=�jI^��sIq���0`YzSH0 �NZ�ˣ:��rk�0C���
+ �ݸP/�����/���Q���#���!�(Y�K2y�6P܋k9p�2�ֽ!��� ����
+ M%��gG�&��g��r?v틁��+X��c�ϙ>�5M_�iv��1s4
+ �s���a��yfUq�UO�<��ݲ��D�H5�:=�D��,ߠ3�/���{�I㝿� ��dvB� oE�֗�'J��l>#J��uAJf��jl\����#����F�[Q�܃m)�ҍXI�|!��؆Rv$ƴ/$W\���2���j,2��1�]]�����V��=p�����y1@�z��P$)�@�=��/�����!{�8�j��hTm��0�W!;݂<��<I��i~U?���HTߪ/�1�L�
+ L���*���EO!��-�|����~ )�GG�7�r�7Q!����e����G���ZC��(�&ln����$>�= ͖�&߮��j�"Bu!m����
+ ��[ ɘex�`O�x�V?��i�Wg��p�����y!�-�t�M�e?��龥?�?�\�B�"g?He%����K�5�W
+ ����x��<Y�/�M"f� H�Z�
+d �r�L��
+ o����ɀu�tt虆�tG��zA�R$��r3����Se(v��\^;���e�ϊ4s�t�u���,Q��� �
+|"D/*
+ �2(���%
+
그런 다음 가져오기 도구와 함께 사용하려면 키를 줄 바꿈 없이 base64로 인코딩해야 한다.
BASE64_KEY=$(base64 -i private_key.der)
+
$ echo $BASE64_KEY
+
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCxAqZGiw7/Et52TRHgLFVYi3HbnMB5m/ZBMu/CZxk6H5zFrCIXcBFh+K58P/rMydFh2rveTd6CT1+szrphe1MPS9mRjvvgy3Bk1XEEUBYOlmIk/eD3lLJEoTjY1E5bpuxirTgV7rR47XEyZsdT6g8Z6s6/M4bjCnJ4ZuBu1VZ3e/5pHrYFSEudy7Xag1Uby1T0Txr/OPPPjVYOA9jgGAO+0wr4elPHyYua1IDnQGfOVuGMKKei+PybKdUvErtp6Z5PJxl7Ylj0Uq8Vym2iXyF7deryXj4vwDZg4UdbX6TcNVSCSM5rvKtAf3S7AsSwflG9WO5Kt1t7QGy2EWVLTrnNAgMBAAECggEASfZJdAB2663+tn/NkFX182GQ2arN4gKBCw01kY1yxQ6gexhJxnFVUhKPFevF5/wMOU8kYOc4qkpwN8zJpCHbuvB+oIuWQ3++HuPwrVSpYr8Dk2FhtxGyy2pyTmentjQxYanvXXq4fi74tY6siyup07KBYPMu0X90BUs3TBhoYNQEKlcZXAR20Y+8NCsKa3QmX9yXUOmDUz5i0zWo7Ojwlig96GpJXq8au3NJcarzFZswYFkGelNIMCDcH05ao8ujOoKecmusMEOGoue1DOzduFAvFRGoo7Cx1C+O2ORR0uwCjyO2H7qrIckoBlnjSzJ5GOY2UNyLAGs5cNEy6Na9IQKBgQDmoSCfx/DSC00lFY7HZ0fsJrQWzhFnj5hyP3bti4GdtCtYxf1jgM+ZPt8SNU1fqWl2+JbDMXM0C65z39bAYcHYeWYHVXGMVU/6vDyeD3l7ohuNi7GwvnejZmN0QWoIpKQ9OeVZWQ243bKg7UTRSDWrOj30RJoS6CzfoKUIM+yCLwKBgQDEe4dJ452/0RsLFhuICQmP9GSUCHZCnSBvRZvWlw6IJ0qL+Ww+fyNKld0BdUFKZsHVamxcEr/e0MMjFYyoq4JGkBJbUZjcg6QIbSn4ENKNWEnEfCGeEf2o2IZSdiTGtC8kV1zAgIoy9he/imosMufovjHLXV2QsuDAplaQwD1wwwKBgQCf0nkxQPV6GarUUCQpu0D0Pb3/L76P7crPIXvhEhQ4nWqMkmgOVG2I3TDpBVchO92CPLL9gX88SfwTAMNpflU/FqHF40hU36oVL+0x+7dMHgLKDEyPFu8BpSq2nb5FTxMh+sUdLcF8ouXu734JKelHR403gXLkN1Ehh8nV7WWwsQKBgEfz9NdaQ6q7KOwmbG6k4JuXJD4R2z0JzZbyJt+u8eNqgCJCdSFt7b6iowylpANbHiDJmGUfeKRgTxXKDni2Vj8BA7ftac1XZ/qt/3CYuIKKknkh/C2m6P2sTYRlP5KE6b6lP5I/gFypQokiZz9IZSUWgaW3y0vyNdxXDdx0iguBAoGBAOR4r7I8WcOM9i/uTSJmoAlI9FqxCmQg6Yly6alMF5jjC2/F2/7byYB1FcZ0EnTomYb4dEePCMron3pBpFIkgx2rcjODvK/hU2Uodpy1XF47mx7dGzuTZYLPijRzl3R/5nW05xnDLBRRAQXaBKLjCYIKBHwiRAEvHioLiDIostz1
+
dev
mode & Transit Enable (Option)다음 명령어로 Vault를 개발 모드로 실행한다.
vault server -dev -dev-root-token-id=root
+
다른 명령창에서 아래 환경변수를 입력한다.
export VAULT_ADDR=http://127.0.0.1:8200
+export VAULT_TOKEN=root
+
환경변수가 적용된 명령창에서 transit
을 활성화 한다.
vault secrets enable transit
+
적절한 키 유형으로 기존 키를 가져온다.
vault transit import transit/keys/my-key $BASE64_KEY type=rsa-2048
+
$ vault transit import transit/keys/my-key $BASE64_KEY type=rsa-2048
+
+Retrieving wrapping key.
+Wrapping source key with ephemeral key.
+Encrypting ephemeral key with wrapping key.
+Submitting wrapped key.
+Success!
+
적용된 키 정보는 기존 transit
의 키 정보를 확인하는 방식과 같다. 키를 가져오면 개인키를 기반으로 공개키가 자동 생성된다.
$ vault read transit/keys/my-key
+
+Key Value
+--- -----
+allow_plaintext_backup false
+auto_rotate_period 0s
+deletion_allowed false
+derived false
+exportable false
+imported_key true
+imported_key_allow_rotation false
+keys map[1:map[creation_time:2023-09-21T17:21:37.857532+09:00 name:rsa-2048 public_key:-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsQKmRosO/xLedk0R4CxV
+WItx25zAeZv2QTLvwmcZOh+cxawiF3ARYfiufD/6zMnRYdq73k3egk9frM66YXtT
+D0vZkY774MtwZNVxBFAWDpZiJP3g95SyRKE42NROW6bsYq04Fe60eO1xMmbHU+oP
+GerOvzOG4wpyeGbgbtVWd3v+aR62BUhLncu12oNVG8tU9E8a/zjzz41WDgPY4BgD
+vtMK+HpTx8mLmtSA50BnzlbhjCinovj8mynVLxK7aemeTycZe2JY9FKvFcptol8h
+e3Xq8l4+L8A2YOFHW1+k3DVUgkjOa7yrQH90uwLEsH5RvVjuSrdbe0BsthFlS065
+zQIDAQAB
+-----END PUBLIC KEY-----
+]]
+latest_version 1
+min_available_version 0
+min_decryption_version 1
+min_encryption_version 0
+name my-key
+supports_decryption true
+supports_derivation false
+supports_encryption true
+supports_signing true
+type rsa-2048
+
기존 transit
의 암호화/복호화 방식과 동일하게 사용한다.
$ vault write transit/encrypt/my-key plaintext=$(echo "This is my data" | base64)
+
+Key Value
+--- -----
+ciphertext vault:v1:Kbiudy2+vK+IRIWnMKPUOwRXPn1eh3KfvvU+59YSPJgidndodgno+7naujQvxpe8T4+ThI01pqw2SeAB6KST8Uh/WVfM91vJ5kWV2NAXJXy+gqe0K3WxzhMQT2DTkxa2mkcUj4WM9blwFW46P9z5SYuphj7ripfiPu1mclolFFD2CUU0WgdW5IzLugWWOOeUlBTh8zQMpdVVVC9xXH8WtPFErXZu1zbo1quDkoR+lLCoyt0ONfcUB24R9oVvP2RjY63Taeu5Phi8DmHDAkAa4T1xB8DbH0wGKBZoK3s2e+GFTfH5XWlxiY832Ds10IuvtbW/TZhkd2Vq1r1bYj3q9w==
+key_version 1
+
+$ echo $(vault write -field=plaintext transit/decrypt/my-key ciphertext=vault:v1:Kbiudy2+vK+IRIWnMKPUOwRXPn1eh3KfvvU+59YSPJgidndodgno+7naujQvxpe8T4+ThI01pqw2SeAB6KST8Uh/WVfM91vJ5kWV2NAXJXy+gqe0K3WxzhMQT2DTkxa2mkcUj4WM9blwFW46P9z5SYuphj7ripfiPu1mclolFFD2CUU0WgdW5IzLugWWOOeUlBTh8zQMpdVVVC9xXH8WtPFErXZu1zbo1quDkoR+lLCoyt0ONfcUB24R9oVvP2RjY63Taeu5Phi8DmHDAkAa4T1xB8DbH0wGKBZoK3s2e+GFTfH5XWlxiY832Ds10IuvtbW/TZhkd2Vq1r1bYj3q9w==) | base64 -d
+
+This is my data
+
이전과 동일한 키를 transit_import
로 추가
$ vault transit import-version transit/keys/my-key $BASE64_KEY type=rsa-2048
+
+Retrieving wrapping key.
+Wrapping source key with ephemeral key.
+Encrypting ephemeral key with wrapping key.
+Submitting wrapped key.
+Success!
+
신규 추가된 키 확인
$ vault read transit/keys/my-key
+Key Value
+--- -----
+allow_plaintext_backup false
+auto_rotate_period 0s
+deletion_allowed false
+derived false
+exportable false
+imported_key true
+imported_key_allow_rotation false
+keys map[1:map[creation_time:2023-09-21T17:21:37.857532+09:00 name:rsa-2048 public_key:-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsQKmRosO/xLedk0R4CxV
+WItx25zAeZv2QTLvwmcZOh+cxawiF3ARYfiufD/6zMnRYdq73k3egk9frM66YXtT
+D0vZkY774MtwZNVxBFAWDpZiJP3g95SyRKE42NROW6bsYq04Fe60eO1xMmbHU+oP
+GerOvzOG4wpyeGbgbtVWd3v+aR62BUhLncu12oNVG8tU9E8a/zjzz41WDgPY4BgD
+vtMK+HpTx8mLmtSA50BnzlbhjCinovj8mynVLxK7aemeTycZe2JY9FKvFcptol8h
+e3Xq8l4+L8A2YOFHW1+k3DVUgkjOa7yrQH90uwLEsH5RvVjuSrdbe0BsthFlS065
+zQIDAQAB
+-----END PUBLIC KEY-----
+] 2:map[creation_time:2023-09-21T17:41:44.047857+09:00 name:rsa-2048 public_key:-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv7H/FBtt07km/w4z/qAK
+sJZumWinnmn5V/5f8TMfQcOjiyF2J4RTTIjUBEGsXF+/bg2w88f39f+r7Ws4wFHa
+91UCgc9MpyQOil42UYN+Rm+kc6hWN26+ZmxqEU/HL1iLPwtu/HGU38MCeS5552M6
+VY7BB7vIhheFyEy8+GDwrjZ3bo+f6Vaya6hyMZ7psS7N5OVaN3z7PsN57lzYaxZ6
+0vVFJeUeeUq371nl7f0cN3eC8PTI8XgQW7Yy8B4lWKHzjpbA2w1hivh6cuXgE2+c
+5MUqkxEKmE8BOMsgm7C+DQ9umQ4q1DkHIWub4oLUg4Tr/VgzECUj+D9tcGLZXFHd
+dwIDAQAB
+-----END PUBLIC KEY-----
+]]
+latest_version 2
+min_available_version 0
+min_decryption_version 1
+min_encryption_version 0
+name my-key
+supports_decryption true
+supports_derivation false
+supports_encryption true
+supports_signing true
+type rsa-2048
+
주의 사항
Vault의 기본 Transit에서 제공하는 키의 순환(rotate
)을 사용하지 않고 사용자가 임의로 새로운 키 버전을 추가하기 때문에 동일한 키가 추가될 수 있다. 이 경우, 동일한 개인 키를 Transit으로 가져오면 신규 버전으로 생성된 키(또는 이전 버전의 키)의 암호화 키 또한 동일한 공개키가 생성되므로, 키 버전과 관계 없이 복호화 된다.
동일한 개인키를 신규 버전 추가하여 기존 공개키와 신규 공개키가 같음을 확인하여 재현할 수 있다.
$ vault read transit/keys/my-key
+Key Value
+--- -----
+allow_plaintext_backup false
+auto_rotate_period 0s
+deletion_allowed false
+derived false
+exportable false
+imported_key true
+imported_key_allow_rotation false
+keys map[1:map[creation_time:2023-09-21T17:21:37.857532+09:00 name:rsa-2048 public_key:-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsQKmRosO/xLedk0R4CxV
+WItx25zAeZv2QTLvwmcZOh+cxawiF3ARYfiufD/6zMnRYdq73k3egk9frM66YXtT
+D0vZkY774MtwZNVxBFAWDpZiJP3g95SyRKE42NROW6bsYq04Fe60eO1xMmbHU+oP
+GerOvzOG4wpyeGbgbtVWd3v+aR62BUhLncu12oNVG8tU9E8a/zjzz41WDgPY4BgD
+vtMK+HpTx8mLmtSA50BnzlbhjCinovj8mynVLxK7aemeTycZe2JY9FKvFcptol8h
+e3Xq8l4+L8A2YOFHW1+k3DVUgkjOa7yrQH90uwLEsH5RvVjuSrdbe0BsthFlS065
+zQIDAQAB
+-----END PUBLIC KEY-----
+] 2:map[creation_time:2023-09-21T17:41:44.047857+09:00 name:rsa-2048 public_key:-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsQKmRosO/xLedk0R4CxV
+WItx25zAeZv2QTLvwmcZOh+cxawiF3ARYfiufD/6zMnRYdq73k3egk9frM66YXtT
+D0vZkY774MtwZNVxBFAWDpZiJP3g95SyRKE42NROW6bsYq04Fe60eO1xMmbHU+oP
+GerOvzOG4wpyeGbgbtVWd3v+aR62BUhLncu12oNVG8tU9E8a/zjzz41WDgPY4BgD
+vtMK+HpTx8mLmtSA50BnzlbhjCinovj8mynVLxK7aemeTycZe2JY9FKvFcptol8h
+e3Xq8l4+L8A2YOFHW1+k3DVUgkjOa7yrQH90uwLEsH5RvVjuSrdbe0BsthFlS065
+zQIDAQAB
+-----END PUBLIC KEY-----
+]]
+latest_version 2
+min_available_version 0
+min_decryption_version 1
+min_encryption_version 0
+name my-key
+supports_decryption true
+supports_derivation false
+supports_encryption true
+supports_signing true
+type rsa-2048
+
동일한 개인키가 Transit 키로 가져오기되고, 따라서 같은 공개키가 생성된다. 앞서 복호화 했던 기존 버전(vault:v1:
)의 ciphertext
의 버전을 신규 버전(vault:v2:
)으로 변경하여 복호화 요청을 하면 복호화 된다.
echo $(vault write -field=plaintext transit/decrypt/my-key ciphertext=vault:v2:Kbiudy2+vK+IRIWnMKPUOwRXPn1eh3KfvvU+59YSPJgidndodgno+7naujQvxpe8T4+ThI01pqw2SeAB6KST8Uh/WVfM91vJ5kWV2NAXJXy+gqe0K3WxzhMQT2DTkxa2mkcUj4WM9blwFW46P9z5SYuphj7ripfiPu1mclolFFD2CUU0WgdW5IzLugWWOOeUlBTh8zQMpdVVVC9xXH8WtPFErXZu1zbo1quDkoR+lLCoyt0ONfcUB24R9oVvP2RjY63Taeu5Phi8DmHDAkAa4T1xB8DbH0wGKBZoK3s2e+GFTfH5XWlxiY832Ds10IuvtbW/TZhkd2Vq1r1bYj3q9w==) | base64 -d
+
+This is my data
+
Rotate 불가
가져온 키는 rotate
기능을 사용할 수 없다.
$ vault write -f transit/keys/my-key/rotate
+
+Error writing data to transit/keys/my-key/rotate: Error making API request.
+
+URL: PUT http://127.0.0.1:8200/v1/transit/keys/my-key/rotate
+Code: 500. Errors:
+
+* 1 error occurred:
+ * imported key my-key does not allow rotation within Vault
+
최소 버전 설정
추가된 키 버전에 대해 최소 버전을 지정하는 것은 가능하다.
$ vault write -f transit/keys/my-key/config min_decryption_version=2
+
+Key Value
+--- -----
+allow_plaintext_backup false
+auto_rotate_period 0s
+deletion_allowed false
+derived false
+exportable false
+imported_key true
+imported_key_allow_rotation false
+keys map[2:map[creation_time:2023-09-21T17:41:44.047857+09:00 name:rsa-2048 public_key:-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsQKmRosO/xLedk0R4CxV
+WItx25zAeZv2QTLvwmcZOh+cxawiF3ARYfiufD/6zMnRYdq73k3egk9frM66YXtT
+D0vZkY774MtwZNVxBFAWDpZiJP3g95SyRKE42NROW6bsYq04Fe60eO1xMmbHU+oP
+GerOvzOG4wpyeGbgbtVWd3v+aR62BUhLncu12oNVG8tU9E8a/zjzz41WDgPY4BgD
+vtMK+HpTx8mLmtSA50BnzlbhjCinovj8mynVLxK7aemeTycZe2JY9FKvFcptol8h
+e3Xq8l4+L8A2YOFHW1+k3DVUgkjOa7yrQH90uwLEsH5RvVjuSrdbe0BsthFlS065
+zQIDAQAB
+-----END PUBLIC KEY-----
+]]
+latest_version 2
+min_available_version 0
+min_decryption_version 2
+min_encryption_version 0
+name my-key
+supports_decryption true
+supports_derivation false
+supports_encryption true
+supports_signing true
+type rsa-2048
+
min_decryption_version
이 2
인 경우, 이보다 낮은 버전의 키 버전의 암호화 된 키는 복호화 할 수 없다.
$ vault write -field=plaintext transit/decrypt/my-key ciphertext=vault:v1:Kbiudy2+vK+IRIWnMKPUOwRXPn1eh3KfvvU+59YSPJgidndodgno+7naujQvxpe8T4+ThI01pqw2SeAB6KST8Uh/WVfM91vJ5kWV2NAXJXy+gqe0K3WxzhMQT2DTkxa2mkcUj4WM9blwFW46P9z5SYuphj7ripfiPu1mclolFFD2CUU0WgdW5IzLugWWOOeUlBTh8zQMpdVVVC9xXH8WtPFErXZu1zbo1quDkoR+lLCoyt0ONfcUB24R9oVvP2RjY63Taeu5Phi8DmHDAkAa4T1xB8DbH0wGKBZoK3s2e+GFTfH5XWlxiY832Ds10IuvtbW/TZhkd2Vq1r1bYj3q9w==
+Error writing data to transit/decrypt/my-key: Error making API request.
+
+URL: PUT http://127.0.0.1:8200/v1/transit/decrypt/my-key
+Code: 400. Errors:
+
+* ciphertext or signature version is disallowed by policy (too old)
+
Transit으로 암호화된 정보는 Vault에서만 복호화
Vault Transit의 암호화된 ciphertext
는 가져오기 전의 개인 키로 복호화 할 수 없다.
(vault:v#:
을 포함하거나 제외한 ciphertext
의 base64 디코드된 데이터)
$ openssl rsautl -decrypt -inkey private_key.pem -in encrypted_data.bin
+
+RSA operation error
+80A095F101000000:error:0200009F:rsa routines:RSA_padding_check_PKCS1_type_2:pkcs decoding error:crypto/rsa/rsa_pk1.c:269:
+80A095F101000000:error:02000072:rsa routines:rsa_ossl_private_decrypt:padding check failed:crypto/rsa/rsa_ossl.c:499:
+
시크릿 엔진 활성화
export VAULT_SKIP_VERIFY=True
+export VAULT_ADDR='http://172.28.128.21:8200'
+export VAULT_TOKEN=<mytoken>
+
$ vault policy write transit-admin - << EOF
+# Enable transit secrets engine
+path "sys/mounts/transit" {
+ capabilities = [ "create", "read", "update", "delete", "list" ]
+}
+
+# To read enabled secrets engines
+path "sys/mounts" {
+ capabilities = [ "read" ]
+}
+
+# Manage the transit secrets engine
+path "transit/*" {
+ capabilities = [ "create", "read", "update", "delete", "list" ]
+}
+EOF
+
$ vault policy write transit-message -<<EOF
+path "transit/encrypt/message" {
+ capabilities = [ "update" ]
+}
+path "transit/decrypt/message" {
+ capabilities = [ "update" ]
+}
+EOF
+
$ vault auth enable userpass
+
+$ vault write auth/userpass/users/transit-admin \
+ password=transit-admin \
+ policies=transit-admin
+
+$ vault write auth/userpass/users/transit-message \
+ password=transit-message \
+ policies=transit-message
+
$ vault login -method userpass username=transit-admin password=transit-admin
+Success! You are now authenticated. The token information displayed below
+is already stored in the token helper. You do NOT need to run "vault login"
+again. Future Vault requests will automatically use this token.
+
+Key Value
+--- -----
+token s.ldJApybiqGBmq3CuBAaqsKXZ
+token_accessor Maek0IMLkOLmFVkpG4DoGUdY
+token_duration 768h
+token_renewable true
+token_policies ["transit-admin"]
+identity_policies []
+policies ["transit-admin"]
+token_meta_username transit-admin
+
$ vault secrets enable -path=transit transit
+
# vault write -f transit/keys/<key_name>
+$ vault write -f transit/keys/message
+
$ VAULT_TOKEN=<client_token> vault write transit/encrypt/message \
+ plaintext=$(base64 <<< "4111 1111 1111 1111")
+
+Key Value
+--- -----
+ciphertext vault:v1:IKfJjYkwv1NWAaw+7O8F0QKcWxu5J98/Wvf0d8yHeBX8AoRajI6BLmS7iniCvkyp
+key_version 1
+
$ VAULT_TOKEN=<client_token> vault write transit/decrypt/message \
+ ciphertext="vault:v1:cZNHVx+sxdMErXRSuDa1q/pz49fXTn1PScKfhf+PIZPvy8xKfkytpwKcbC0fF2U="
+
+Key Value
+--- -----
+plaintext Y3JlZGl0LWNhcmQtbnVtYmVyCg==
+
+$ base64 --decode <<< "Y3JlZGl0LWNhcmQtbnVtYmVyCg=="
+4111 1111 1111 1111
+
또는
$ vault write -field=plaintext transit/decrypt/message ciphertext="vault:v1:cZNHVx+sxdMErXRSuDa1q/pz49fXTn1PScKfhf+PIZPvy8xKfkytpwKcbC0fF2U=" | base64 --decode
+4111 1111 1111 1111
+
$ vault write -f transit/keys/message/rotate
+$ vault read transit/keys/message
+Key Value
+--- -----
+allow_plaintext_backup false
+deletion_allowed false
+derived false
+exportable false
+keys map[1:1617699577 2:1617705005]
+latest_version 2
+min_available_version 0
+min_decryption_version 1
+min_encryption_version 0
+name message
+supports_decryption true
+supports_derivation true
+supports_encryption true
+supports_signing false
+type aes256-gcm96
+
$ vault write transit/encrypt/message \
+ plaintext=$(base64 <<< "4111 1111 1111 1111")
+Key Value
+--- -----
+ciphertext vault:v2:wdEpTdasoqY0I9SWfj0r93fDevsIl2cX2aAdfDqAPmvCMAf2w/2blU+k86MVscgW
+key_version 2
+
$ vault write transit/rewrap/message \
+ ciphertext="vault:v1:+msBmr5zjE7ZOaA1h9/kV7ZWGZlZX+YEzgco70wTT+lvlfxUDLIgdFGFVOYN777X"
+
+Key Value
+--- -----
+ciphertext vault:v2:kChHZ9w4ILRfw+DzO53IZ8m5PyB2yp2/tKbub34uB+iDqtDRB+NLCPrpzTtJHJ4=
+
$ vault write transit/keys/message/config min_decryption_version=2
+$ vault read transit/keys/message
+Key Value
+--- -----
+...
+keys map[2:1617705005]
+...
+
$ vault write -field=plaintext transit/decrypt/message ciphertext="vault:v1:KBhy3R8Po4J7tRtkJzZId7DZIpugxMFpTkwPwq3JOy60t1sq149PB8mmPqhKBVLT" | base64 --decode
+
+Error writing data to transit/decrypt/message: Error making API request.
+
+URL: PUT http://172.28.128.21:8200/v1/transit/decrypt/message
+Code: 400. Errors:
+
+* ciphertext or signature version is disallowed by policy (too old)
+
https://developer.hashicorp.com/vault/docs/auth/aws
AWS auth method는 IAM 계정 또는 EC2 인스턴스에 대한 Vault 토큰을 검색하는 자동화된 메커니즘을 제공한다. 이 방식은 다양한 상황에서 운영자가 보안에 민감한 자격증명(토큰, 사용자 이름/비밀번호, 클라이언트 인증서 등)을 수동으로 먼저 생성할 필요가 없다.
AWS auth method는 iam
과 ec2
방식을 지원한다.
iam
인증 방식AWS IAM 자격 증명으로 서명된 AWS 요청이 인증에 사용된다. 거의 모든 AWS 리소스는 AWS 보안 토큰 서비스(STS)를 호출하여 자신의 신원을 조회할 수 있다. Vault의 AWS iam 인증 방식은 사용자가 직접 요청을 보내는 대신, 서명된 요청 데이터를 Vault로 전송하여 STS에 서명된 요청을 생성할 수 있도록 함으로써 이러한 이점을 활용한다. Vault는 요청을 실행하고 AWS(다시 말해서 신뢰할 수 있는 제3자)로부터 사용자의 실제 신원을 확인한다. iam
방식이 좀더 최신의 방식이며, 기존 ec2
방식의 한계였던 람다 또는 ECS 같은 다양한 유형의 서비스에서 작동한다.
AWS STS API에는 클라이언트의 신원을 확인할 수 있는 메서드인 sts:GetCallerIdentity
가 포함되어 있다. 클라이언트는 AWS 서명 v4 알고리즘을 사용하여 GetCallerIdentity
쿼리에 서명하고 이를 Vault 서버로 보낸다. GetCallerIdentity
요청에 서명하는 데 사용되는 자격 증명은 EC2 인스턴스에 대한 EC2 인스턴스 메타데이터 서비스 또는 AWS Lambda 함수 실행의 AWS 환경 변수에서 가져올 수 있으므로 운영자가 먼저 신원 자료를 수동으로 프로비저닝할 필요가 없다. 그리고 사용되는 자격 증명은 원칙적으로 AWS내부의 Role이부여된 리소스 뿐만 아니라 Access Key를 사용하여 어디에서나 가져올 수 있다.
GetCallerIdentity
쿼리는 Request URL
, Request Body
, Request Header
, Request Method
의 네 가지 정보로 구성되며, AWS 서명은 이러한 필드를 통해 계산된다. Vault 서버는 이 정보를 사용해 쿼리를 재구성하고 AWS STS 서비스로 전달한다. STS 서비스의 응답에 따라 서버는 클라이언트를 인증한다.
클라이언트가 AWS STS API 엔드포인트와 통신하기 위한 네트워크 접근이 필요하지 않으며, 요청에 서명하기 위한 자격 증명에 대한 액세스만 있으면 된다. 그러나 Vault 서버가 STS 엔드포인트로 요청을 전송하려면 네트워크 수준 액세스가 필요하다.
서명된 각 AWS 요청에는 현재 타임스탬프가 포함되어 리플레이 공격의 위험을 완화합니다. 또한, Vault에서는 다양한 유형의 리플레이 공격(예: 개발 Vault 인스턴스에서 서명된 GetCallerIdentity
요청을 도용하여 프로덕션 Vault 인스턴스에 인증하는 데 사용하는 공격)을 완화하기 위해 추가 헤더인 X-Vault-AWS-IAM-Server-ID를 요구할 수 있다. 또한 Vault는 이 헤더가 AWS 서명에 포함된 헤더 중 하나이어야 하며, 해당 서명을 인증하기 위해 AWS에 의존한다.
AWS API 엔드포인트는 서명된 GET 요청과 POST 요청을 모두 지원하지만, 간단하게 하기 위해 aws auth 메서드는 POST 요청만 지원합니다. 또한 사전 서명된 요청, 즉 인증 정보가 포함된 X-Amz-Credential
, X-Amz-Signature
, X-Amz-SignedHeaders
GET 쿼리 매개 변수가 있는 요청은 지원하지 않는다.
또한 GetCallerIdentity
호출과 관련하여 어떤 종류의 권한 부여도 포함하지 않는다. 예를 들어, 자격 증명에 대해 모든 액세스가 MFA 인증을 받아야 하는 IAM 정책이 있는 경우, MFA 인증을 받지 않은 자격 증명(즉, GetSessionToken
을 호출하고 MFA 코드를 제공하여 검색한 자격 증명이 아닌 원시 자격 증명)은 이 방법을 사용하여 여전히 Vault에 인증할 수 있다. Vault에 인증하는 동안 IAM 주체가 MFA 인증을 받도록 강제하는 것은 불가능하다.
ec2
인증 방식ec2 방식에서는 AWS가 신뢰할 수 있는 제3자로 취급되며, 각 EC2 인스턴스를 고유하게 나타내는 암호화 서명된 동적 메타데이터 정보가 인증에 사용된다. 이 메타데이터 정보는 AWS가 모든 EC2 인스턴스에 자동으로 제공한다. 특정 AMI, 특정 인스턴스 프로파일의 EC2 인스턴스 또는 특수 태그 값을 가진 EC2 인스턴스(role_tag 기능을 통해)에서 EC2 인스턴스에 대한 액세스를 제한하는 등 EC2 인스턴스를 처리하는 데 특화되어 있다.
Amazon EC2 인스턴스는 인스턴스를 설명하는 메타데이터에 액세스할 수 있습니다. Vault EC2 인증 메서드는 이 메타데이터의 구성 요소를 활용하여 초기 Vault 토큰을 인증하고 EC2 인스턴스에 배포한다. 데이터 흐름(아래 그래픽에도 표시됨)은 다음과 같다:
{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "ec2:DescribeInstances",
+ "iam:GetInstanceProfile",
+ "iam:GetUser",
+ "iam:GetRole"
+ ],
+ "Resource": "*"
+ },
+ {
+ "Effect": "Allow",
+ "Action": ["sts:AssumeRole"],
+ "Resource": ["arn:aws:iam::<AccountId>:role/<VaultRole>"]
+ },
+ {
+ "Sid": "ManageOwnAccessKeys",
+ "Effect": "Allow",
+ "Action": [
+ "iam:CreateAccessKey",
+ "iam:DeleteAccessKey",
+ "iam:GetAccessKeyLastUsed",
+ "iam:GetUser",
+ "iam:ListAccessKeys",
+ "iam:UpdateAccessKey"
+ ],
+ "Resource": "arn:aws:iam::*:user/${aws:username}"
+ },
+ {
+ "Sid": "IAM_Method",
+ "Effect": "Allow",
+ "Action": [
+ "sts:GetCallerIdentity"
+ ],
+ "Resource": "*"
+ }
+ ]
+}
+
+
arn:aws:iam::\<AccountId\>:*
: 로 구성하면 AWS 계정의 모든 주체가 이 계정에 로그인arn:aws:iam::\<AccountId\>:role/*
로 구성하면 AWS 계정의 모든 IAM 역할이 해당 계정에 로그인iam:GetUser
및 iam:GetRole
권한을 부여arn:aws:iam::123456789012:role/MyRole
인 역할이 있는 경우, 해당 역할에서 AssumeRole을 호출하여 반환되는 자격 증명은 arn:aws:sts::123456789012:assumed-role/MyRole/RoleSessionName
이며, 여기서 RoleSessionName
은 AssumeRole API 호출의 세션 이름ec2:DescribeInstances
는 ec2 인증 메서드를 사용하거나 EC2 인스턴스가 role binding 요구 사항을 충족하는지 확인하기 위해 ec2_instance
엔티티 유형을 추론할 때 필요iam:GetInstanceProfile
은 ec2 인증 메서드에 bound_iam_role_arn
이 있을 때 사용sts:AssumeRole
구문이 필요(지정된 리소스는 계정 간 액세스를 구성한 모든 역할의 목록이어야 하며, 각 역할에는 이 IAM 정책이 첨부되어 있어야 하며 sts:AssumeRole
문은 제외)ManageOwnAccessKeys
구절이 필요AWS외부에서 구성하는 경우 AWS 인증을 위한 Policy가 구성된 Access Key/Secret Key가 필요하다.
aws
auth method 활성화$ vault auth enable aws
+
$ vault write auth/aws/config/client secret_key=vCtSM8ZUEQ3mOFVlYPBQkf2sO6F/W7a5TVzrl3Oj access_key=VKIAJBRHKH6EVTTNXDHA
+
ec2
role 구성 및 로그인$ vault write auth/aws/role/dev-role auth_type=ec2 bound_ami_id=ami-fce3c696 policies=prod,dev max_ttl=500h
+
+$ SIGNATURE=$(curl -s http://169.254.169.254/latest/dynamic/instance-identity/rsa2048 | tr -d '\n')
+
+$ vault write auth/aws/login role=dev-role pkcs7=$SIGNATURE
+
iam
role 구성 및 로그인$ vault write auth/aws/role/dev-role-iam auth_type=iam bound_iam_principal_arn=arn:aws:iam::123456789012:role/MyRole policies=prod,dev max_ttl=500h
+
+$ vault login -method=aws header_value=vault.example.com role=dev-role-iam \
+ aws_access_key_id=<access_key> \
+ aws_secret_access_key=<secret_key> \
+ aws_security_token=<security_token>
+
+# AWS SDK가 지원하는 인증 방식이 설정되어있는 경우 aws_access_key_id 생략 가능
+# ~/.aws/credentials
+# IAM 인스턴스 프로파일
+# ECS task role 등
+$ vault login -method=aws header_value=vault.example.com role=dev-role-iam
+
+# 리전 지정이 필요한 경우 대상 지정
+$ vault login -method=aws region=us-west-2 role=dev-role-iam
+
+# 요청 매개변수를 생성하여 로그인 메서드에 전달
+vault write auth/aws/login role=dev-role-iam \
+ iam_http_request_method=POST \
+ iam_request_url=aHR0cHM6Ly9zdHMuYW1hem9uYXdzLmNvbS8= \
+ iam_request_body=QWN0aW9uPUdldENhbGxlcklkZW50aXR5JlZlcnNpb249MjAxMS0wNi0xNQ== \
+ iam_request_headers=eyJDb250ZW50LUxlbmd0aCI6IFsiNDMiXSwgIlVzZXItQWdlbnQiOiBbImF3cy1zZGstZ28vMS40LjEyIChnbzEuNy4xOyBsaW51eDsgYW1kNjQpIl0sICJYLVZhdWx0LUFXU0lBTS1TZXJ2ZXItSWQiOiBbInZhdWx0LmV4YW1wbGUuY29tIl0sICJYLUFtei1EYXRlIjogWyIyMDE2MDkzMFQwNDMxMjFaIl0sICJDb250ZW50LVR5cGUiOiBbImFwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZDsgY2hhcnNldD11dGYtOCJdLCAiQXV0aG9yaXphdGlvbiI6IFsiQVdTNC1ITUFDLVNIQTI1NiBDcmVkZW50aWFsPWZvby8yMDE2MDkzMC91cy1lYXN0LTEvc3RzL2F3czRfcmVxdWVzdCwgU2lnbmVkSGVhZGVycz1jb250ZW50LWxlbmd0aDtjb250ZW50LXR5cGU7aG9zdDt4LWFtei1kYXRlO3gtdmF1bHQtc2VydmVyLCBTaWduYXR1cmU9YTY5ZmQ3NTBhMzQ0NWM0ZTU1M2UxYjNlNzlkM2RhOTBlZWY1NDA0N2YxZWI0ZWZlOGZmYmM5YzQyOGMyNjU1YiJdfQ==
+
iam_request_url 값 예시 : https://sts.amazonaws.com/
iam_request_body 값 예시 : Action=GetCallerIdentity&Version=2011-06-15
iam_request_headers 값 예시 :
{
+ "Content-Length": [
+ "43"
+ ],
+ "User-Agent": [
+ "aws-sdk-go/1.4.12 (go1.7.1; linux; amd64)"
+ ],
+ "X-Vault-AWSIAM-Server-Id": [
+ "vault.example.com"
+ ],
+ "X-Amz-Date": [
+ "20160930T043121Z"
+ ],
+ "Content-Type": [
+ "application/x-www-form-urlencoded; charset=utf-8"
+ ],
+ "Authorization": [
+ "AWS4-HMAC-SHA256 Credential=foo/20160930/us-east-1/sts/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-date;x-vault-server, Signature=a69fd750a3445c4e553e1b3e79d3da90eef54047f1eb4efe8ffbc9c428c2655b"
+ ]
+}
+
AWS 내부에서 구성된 Vault 서버 인스턴스 및 클라이언트에 프로파일을 구성하여 Access Key를 생략하고 인증을 구현할 수 있다.
terraform {
+ required_providers {
+ aws = {
+ source = "hashicorp/aws"
+ version = "5.5.0"
+ }
+ random = {
+ source = "hashicorp/random"
+ version = "3.5.1"
+ }
+ tls = {
+ source = "hashicorp/tls"
+ version = "3.0.0"
+ }
+ }
+}
+
+provider "aws" {
+ region = "ap-northeast-2"
+
+ default_tags {
+ tags = {
+ Environment = "Demo"
+ Owner = "gs@hashicorp.com"
+ Project = "example"
+ }
+ }
+}
+
+data "aws_caller_identity" "current" {}
+
+resource "aws_vpc" "example" {
+ cidr_block = "10.0.0.0/16"
+ enable_dns_support = true
+ enable_dns_hostnames = true
+}
+
+data "aws_availability_zones" "available" {
+ state = "available"
+}
+
+// public subnet
+resource "aws_subnet" "public" {
+ vpc_id = aws_vpc.example.id
+ availability_zone = data.aws_availability_zones.available.names.0
+ cidr_block = cidrsubnet(aws_vpc.example.cidr_block, 8, 0) // "10.0.0.0/24" & "10.0.1.0/24"
+ map_public_ip_on_launch = true
+}
+
+resource "aws_internet_gateway" "public" {
+ vpc_id = aws_vpc.example.id
+}
+
+resource "aws_route_table" "public" {
+ vpc_id = aws_vpc.example.id
+
+ route {
+ cidr_block = "0.0.0.0/0"
+ gateway_id = aws_internet_gateway.public.id
+ }
+}
+
+resource "aws_route_table_association" "public" {
+ subnet_id = aws_subnet.public.id
+ route_table_id = aws_route_table.public.id
+}
+
+resource "aws_eip" "public" {
+ domain = "vpc"
+}
+
+resource "aws_nat_gateway" "public" {
+ allocation_id = aws_eip.public.id
+ subnet_id = aws_subnet.public.id
+}
+
+// SG
+resource "aws_security_group" "example" {
+ name = "example"
+ vpc_id = aws_vpc.example.id
+
+ egress {
+ from_port = 0
+ to_port = 0
+ protocol = "-1"
+ cidr_blocks = ["0.0.0.0/0"]
+ ipv6_cidr_blocks = ["::/0"]
+ }
+}
+
+resource "aws_security_group_rule" "example_ssh" {
+ type = "ingress"
+ from_port = 22
+ to_port = 22
+ protocol = "tcp"
+ cidr_blocks = ["0.0.0.0/0"]
+ security_group_id = aws_security_group.example.id
+}
+
+resource "aws_security_group_rule" "example_vault" {
+ type = "ingress"
+ from_port = 8200
+ to_port = 8200
+ protocol = "tcp"
+ cidr_blocks = ["0.0.0.0/0"]
+ security_group_id = aws_security_group.example.id
+}
+
+// key pair
+resource "tls_private_key" "ssh" {
+ algorithm = "RSA"
+ rsa_bits = 4096
+}
+
+resource "local_sensitive_file" "ssh_private" {
+ content = tls_private_key.ssh.private_key_pem
+ filename = "${path.module}/ssh_private"
+}
+
+resource "random_id" "key_id" {
+ keepers = {
+ ami_id = tls_private_key.ssh.public_key_openssh
+ }
+
+ byte_length = 8
+}
+
+resource "aws_key_pair" "ssh" {
+ key_name = "key-${random_id.key_id.hex}"
+ public_key = tls_private_key.ssh.public_key_openssh
+}
+
+// EC2
+data "aws_ami" "ubuntu" {
+ most_recent = true
+
+ filter {
+ name = "name"
+ values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
+ }
+
+ filter {
+ name = "virtualization-type"
+ values = ["hvm"]
+ }
+
+ owners = ["099720109477"] # Canonical
+}
+
+data "aws_iam_policy_document" "example_instance_role_client" {
+ statement {
+ effect = "Allow"
+ actions = ["sts:AssumeRole"]
+ principals {
+ type = "Service"
+ identifiers = ["ec2.amazonaws.com"]
+ }
+ }
+}
+
+resource "aws_iam_role" "example_client_instance_role_client" {
+ name_prefix = "auth-example-iam-role-client"
+ assume_role_policy = data.aws_iam_policy_document.example_instance_role_client.json
+}
+
+resource "aws_iam_instance_profile" "example_instance_profile_client" {
+ path = "/"
+ role = aws_iam_role.example_client_instance_role_client.name
+}
+
+resource "aws_instance" "client" {
+ ami = data.aws_ami.ubuntu.id
+ iam_instance_profile = aws_iam_instance_profile.example_instance_profile_client.name
+ instance_type = "t3.micro"
+ key_name = aws_key_pair.ssh.key_name
+ subnet_id = aws_subnet.public.id
+ associate_public_ip_address = true
+ vpc_security_group_ids = [aws_security_group.example.id]
+
+ user_data = <<EOF
+ #! /bin/bash
+ sudo apt update && sudo apt install gpg
+ wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
+ gpg --no-default-keyring --keyring /usr/share/keyrings/hashicorp-archive-keyring.gpg --fingerprint
+ echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
+ sudo apt update
+ sudo apt install vault
+ EOF
+
+ tags = {
+ Name = "client"
+ }
+}
+
+data "aws_iam_policy_document" "assume_role" {
+ statement {
+ effect = "Allow"
+
+ principals {
+ type = "Service"
+ identifiers = ["ec2.amazonaws.com"]
+ }
+
+ actions = ["sts:AssumeRole"]
+ }
+}
+
+resource "aws_iam_role" "vault_role" {
+ name = "vault-role"
+ assume_role_policy = data.aws_iam_policy_document.assume_role.json
+}
+
+data "aws_iam_policy_document" "example_policy_vault" {
+ statement {
+ effect = "Allow"
+ actions = [
+ "ec2:DescribeInstances",
+ "iam:GetInstanceProfile",
+ "iam:GetUser",
+ "iam:GetRole",
+ ]
+ resources = [
+ "*"
+ // "arn:aws:iam::*:user/*",
+ // "arn:aws:iam::*:role/*",
+ ]
+ }
+ statement {
+ effect = "Allow"
+ actions = [
+ "sts:AssumeRole",
+ ]
+ resources = [
+ "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/${aws_iam_role.vault_role.name}"
+ ]
+ }
+ statement {
+ effect = "Allow"
+ actions = [
+ "sts:GetCallerIdentity"
+ ]
+ resources = ["*"]
+ }
+ statement {
+ sid = "ManageOwnAccessKeys"
+ actions = [
+ "iam:CreateAccessKey",
+ "iam:DeleteAccessKey",
+ "iam:GetAccessKeyLastUsed",
+ "iam:GetUser",
+ "iam:ListAccessKeys",
+ "iam:UpdateAccessKey",
+ ]
+ resources = ["arn:aws:iam::*:user/$${aws:username}"]
+ }
+}
+
+resource "aws_iam_policy" "policy" {
+ name = "vault-policy"
+ description = "A test policy"
+ policy = data.aws_iam_policy_document.example_policy_vault.json
+}
+
+resource "aws_iam_role_policy_attachment" "vault-attach" {
+ role = aws_iam_role.vault_role.name
+ policy_arn = aws_iam_policy.policy.arn
+}
+
+resource "aws_iam_instance_profile" "vault_profile" {
+ name = "vault_profile"
+ role = aws_iam_role.vault_role.name
+}
+
+resource "aws_instance" "vault" {
+ ami = data.aws_ami.ubuntu.id
+ iam_instance_profile = aws_iam_instance_profile.vault_profile.name
+ instance_type = "t3.micro"
+ key_name = aws_key_pair.ssh.key_name
+ subnet_id = aws_subnet.public.id
+ associate_public_ip_address = true
+ vpc_security_group_ids = [aws_security_group.example.id]
+
+ user_data = <<EOF
+ #! /bin/bash
+ sudo apt update && sudo apt install gpg
+ wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
+ gpg --no-default-keyring --keyring /usr/share/keyrings/hashicorp-archive-keyring.gpg --fingerprint
+ echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
+ sudo apt update
+ sudo apt install vault
+ EOF
+
+ tags = {
+ Name = "vault"
+ }
+}
+
+output "info" {
+ value = {
+ client_ip = aws_instance.client.public_ip
+ vault_ip = aws_instance.vault.public_ip
+ client_role_arn = aws_iam_role.example_client_instance_role_client.arn
+ ami_id = aws_instance.client.ami
+ ec2_id = aws_instance.client.id
+ private_key = nonsensitive(tls_private_key.ssh.private_key_pem)
+ }
+}
+
ssh_private
파일로 자동 생성$ ssh -i ./ssh_private $(terraform output -raw vault_ip)
+
# Server Instance
+$ vault server -dev -dev-root-token-id=root -log-level=trace
+
# Client Instance
+$ export VAULT_ADDR=http://10.0.0.8:8200
+$ export VAULT_TOKEN=root
+
+$ vault policy write "example-policy" -<<EOF
+path "secret/data/example_*" {
+ capabilities = ["create", "read"]
+}
+EOF
+
+$ vault kv put secret/example_test foo=bar
+
+$ vault auth enable aws
+
+# auth 구성에 access key 선언이 생략
+$ vault write auth/aws/config/client sts_region="ap-northeast-2"
+
+# 만약 Vault Server의 AWS Auth에 권한이 없는 경우
+$ vault write \
+ auth/aws/role/example-role-name \
+ auth_type=iam \
+ policies=example-policy \
+ max_ttl=500h \
+ bound_iam_principal_arn=$client_instance_role_arn \
+ inferred_entity_type="ec2_instance" \
+ inferred_aws_region="ap-northeast-2"
+
+Error writing data to auth/aws/role/example-role-name: Error making API request.
+
+URL: PUT http://10.0.0.86:8200/v1/auth/aws/role/example-role-name
+Code: 400. Errors:
+
+* unable to resolve ARN "arn:aws:iam::467567795630:role/auth-example-iam-role20230630045547898800000001" to internal ID: unable to fetch current caller: NoCredentialProviders: no valid providers in chain. Deprecated.
+ For verbose messaging see aws.Config.CredentialsChainVerboseErrors
+
+
+# Policy 추가 후 login 수행
+$ vault login -method=aws role=example-role-name
+
+Key Value
+--- -----
+token hvs.CAESIGe7HuhqFefKHDkE_M_leja0bRDEnwPZs7CeztZQXuVCGh4KHGh2cy41S2VienFmbU5scFZBcmFpWkZNemtrdmE
+token_accessor VAD61CRZHhLp7VN6Uf6qRHbh
+token_duration 500h
+token_renewable true
+token_policies ["default" "example-policy"]
+identity_policies []
+policies ["default" "example-policy"]
+token_meta_account_id 467567795630
+token_meta_auth_type iam
+token_meta_role_id c1de423d-0751-a879-7b43-1047f1c43a42
+
+
+$ VAULT_TOKEN=hvs.CAESIGe7HuhqFefKHDkE_M_leja0bRDEnwPZs7CeztZQXuVCGh4KHGh2cy41S2VienFmbU5scFZBcmFpWkZNemtrdmE vault kv get secret/example_test
+
+====== Secret Path ======
+secret/data/example_test
+
+======= Metadata =======
+Key Value
+--- -----
+created_time 2023-06-30T06:21:09.409042961Z
+custom_metadata <nil>
+deletion_time n/a
+destroyed false
+version 1
+
+=== Data ===
+Key Value
+--- -----
+foo bar
+
HashiCorp Learn - Login MFA : https://learn.hashicorp.com/tutorials/vault/multi-factor-authentication
Configure TOTP MFA Method : https://www.vaultproject.io/api-docs/secret/identity/mfa/totp
Vault Login MFA Overview : https://www.vaultproject.io/docs/auth/login-mfa
1.10.3+ recommend : https://discuss.hashicorp.com/t/vault-1-10-3-released/39394
$ ROOT_TOKEN=hvs...
+$ VAULT_ADDR=https://<your-vault-addr>:8200
+$ MY_PASSWORD=password
+
+# If you have NAMESPACE with Enterprise
+$ export VAULT_NAMESPACE=admin
+
$ VAULT_TOKEN=$ROOT_TOKEN vault auth enable userpass
+
+$ USERPASS_ACCESSOR=$(VAULT_TOKEN=$ROOT_TOKEN vault auth list | grep userpass | awk '{print $3}')
+
+$ VAULT_TOKEN=$ROOT_TOKEN vault write auth/userpass/users/admin password=$MY_PASSWORD
+
$ ENTITY_ID=$(VAULT_TOKEN=$ROOT_TOKEN vault write -field=id identity/entity name="admin")
+
+echo $ENTITY_ID
+
+$ VAULT_TOKEN=$ROOT_TOKEN vault write identity/entity-alias \
+ name="admin" \
+ canonical_id="$ENTITY_ID" \
+ mount_accessor="$USERPASS_ACCESSOR"
+
https://www.vaultproject.io/api-docs/secret/identity/mfa/totp#parameters
$ METHOD_ID=$(vault write -field=method_id identity/mfa/method/totp issuer=HCP-Vault period=30 key_size=30 qr_size=200 algorithm=SHA256 digits=6 name=admin)
+
+$ echo $METHOD_ID
+
+$ vault read identity/mfa/method/totp/$METHOD_ID
+
+# vault write identity/mfa/method/totp/generate method_id=$METHOD_ID
+$ vault write identity/mfa/method/totp/admin-generate method_id=$METHOD_ID entity_id=$ENTITY_ID
+
+Key Value
+--- -----
+barcode iVBORw0KGgoAAAANSUhEUgAAAM...
+url otpauth://totp/Vault:307d6c16-6f5c...
+
$ VAULT_TOKEN=$ROOT_TOKEN vault write identity/mfa/login-enforcement/mylogin \
+ mfa_method_ids="$METHOD_ID" \
+ auth_method_accessors="$USERPASS_ACCESSOR"
+
That's able to use online QR generator
$ vault secrets enable totp
+
+$ vault write totp/keys/hcp-vault url="otpauth://totp/HCP-Vault:0d0cf6f5-62e6-6914-5070-47e997e2aa..."
+
+$ vault read totp/code/hcp-vault
+Key Value
+--- -----
+code 714908
+
$ vault login -method userpass username=admin password=$MY_PASSWORD
+
+Enter the passphrase for methodID "0b9d2229-5d64-dc5d-87cc-0fd22775b918" of
+type "totp": <enter_totp>
+
주의
해당 방법은 username/password 방식의 Admin권한의 사용자를 생성하나,
보안상 실 구성에는 권장하지 않습니다.
vault auth enable userpass
+
vault policy write super-user - << EOF
+path "*" {
+capabilities = ["create", "read", "update", "delete", "list", "sudo"]
+}
+EOF
+
$policy = @"
+path "*" {
+ capabilities = ["create", "read", "update", "delete", "list", "sudo"]
+}
+"@
+
+vault policy write super-user - << $policy
+
vault write auth/userpass/users/admin password=password policies=super-user
+
vault login -method userpass username=admin password=password
+Success! You are now authenticated. The token information displayed below
+is already stored in the token helper. You do NOT need to run "vault login"
+again. Future Vault requests will automatically use this token.
+
+Key Value
+--- -----
+token s.vIRSNJYiVMtRFuOwq4pKbYGK
+token_accessor EQz974TUJCy0iV7ALZ8xzGe4
+token_duration 768h
+token_renewable true
+token_policies ["default" "super-user"]
+identity_policies []
+policies ["default" "super-user"]
+token_meta_username admin
+
vault token create -policy=super-user
+Key Value
+--- -----
+token s.DgfYH1dUBxAbBDuPM5U2kkym
+token_accessor 6GJDO2KH4yPK0jStQw8gJ63r
+token_duration 768h
+token_renewable true
+token_policies ["default" "super-user"]
+identity_policies []
+policies ["default" "super-user"]
+
별도 Auth Method를 사용하지 않고 Token으로만 사용하는 경우 Token에 대한 role을 생성하여 해당 role의 정의된 설정에 종속된 Token을 생성할 수 있음
UI > Access > Entities > [create entity] : 100y-entity
entity에서 aliases 생성 : 100y-alias
role 생성 (payload.json)
{
+ "allowed_policies": [
+ "my-policy"
+ ],
+ "name": "100y",
+ "orphan": false,
+ "bound_cidrs": ["127.0.0.1/32", "128.252.0.0/16"],
+ "renewable": true,
+ "allowed_entity_aliases": ["100y-alias"]
+}
+
role 적용
curl -H "X-Vault-Token: hvs.QKRiVmCedA06dCSc2TptmSk1" -X POST --data @payload.json http://127.0.0.1:8200/v1/auth/token/roles/100y
+
role에 대한 사용자 정의 tune 적용(옵션)
vault auth tune -max-lease-ttl=876000h token/role/100y
+vault auth tune -default-lease-ttl=876000h token/role/100y
+
tune 적용된 role 확인
$ vault read auth/token/roles/100y
+
+Key Value
+--- -----
+allowed_entity_aliases [100y-alias]
+allowed_policies [default]
+allowed_policies_glob []
+bound_cidrs [127.0.0.1 128.252.0.0/16]
+disallowed_policies []
+disallowed_policies_glob []
+explicit_max_ttl 0s
+name 100y
+orphan false
+path_suffix n/a
+period 0s
+renewable true
+token_bound_cidrs [127.0.0.1 128.252.0.0/16]
+token_explicit_max_ttl 0s
+token_no_default_policy false
+token_period 0s
+token_type default-service
+
token 생성
$ vault token create -entity-alias=100y-alias -role=100y
+Key Value
+--- -----
+token hvs.CAESIIveQyE34VOowkCXj4InopxsQHWXu2iW00UQDDCTb-pIGh4KHGh2cy5UZGJ4MjJic1RjY1BlVGRWVHhzNFgwWW4
+token_accessor Cx6qjyUGwqPmqoPNe9tmkCiN
+token_duration 876000h
+token_renewable true
+token_policies ["default"]
+identity_policies ["default"]
+policies ["default"]
+
token이 role의 구성이 반영되었는지 확인
$ vault token lookup hvs.CAESIIveQyE34VOowkCXj4InopxsQHWXu2iW00UQDDCTb-pIGh4KHGh2cy5UZGJ4MjJic1RjY1BlVGRWVHhzNFgwWW4
+
+Key Value
+--- -----
+accessor Cx6qjyUGwqPmqoPNe9tmkCiN
+bound_cidrs [127.0.0.1 128.252.0.0/16]
+creation_time 1651059486
+creation_ttl 876000h
+display_name token
+entity_id 53fc4716-fc0d-db34-14b8-ab4258f89fb1
+expire_time 2122-04-03T20:38:06.73198+09:00
+explicit_max_ttl 0s
+external_namespace_policies map[]
+id hvs.CAESIIveQyE34VOowkCXj4InopxsQHWXu2iW00UQDDCTb-pIGh4KHGh2cy5UZGJ4MjJic1RjY1BlVGRWVHhzNFgwWW4
+identity_policies [default]
+issue_time 2022-04-27T20:38:06.731984+09:00
+meta <nil>
+num_uses 0
+orphan false
+path auth/token/create/100y
+policies [default]
+renewable true
+role 100y
+ttl 875999h59m3s
+type service
+
vault auth list -format=json | jq -r '.["token/"].accessor' > accessor_token.txt
+
+vault write -format=json identity/entity name="100y-entity" policies="default" \
+ metadata=organization="HC" \
+ metadata=team="QA" \
+ | jq -r ".data.id" > entity_id.txt
+
+vault write identity/entity-alias name="100y-alias" \
+ canonical_id=$(cat entity_id.txt) \
+ mount_accessor=$(cat accessor_token.txt) \
+ custom_metadata=account="QA Account"
+
+vault write auth/token/roles/100y allowed_policies="super-user" orphan=false bound_cidrs="127.0.0.1/32,128.252.0.0/16" renewable=true allowed_entity_aliases="100y-alias" token_period="876000h"
+
+vault auth tune -max-lease-ttl=876000h token/role/100y
+
+vault auth tune -default-lease-ttl=876000h token/role/100y
+
+vault token create -entity-alias=100y-alias -role=100y
+Key Value
+--- -----
+token hvs.CAESIDv-SKwwf3MS-CAutW7aQgAZRBjh01lYLeriuSYzYIwfGiEKHGh2cy50cXFIYVhneDBVYU1OT1hXbWc3WHdtbzUQsgU
+token_accessor TAAPfxaUX1nx64ZqrLPa1VHx
+token_duration 876000h
+token_renewable true
+token_policies ["default" "super-user"]
+identity_policies ["default"]
+policies ["default" "super-user"]
+
사용자별 UI 접근에 대한 설정을 Kv-v2를 예로 확인
UI 접근을 위해서는 metadata
에 대한 권한 추가가 필요함
$ vault policy write ui-kv-policy - << EOF
+
+path "kv-v2/data/path/" {
+ capabilities = ["create", "update", "read", "delete", "list"]
+}
+path "kv-v2/delete/path/" {
+ capabilities = ["update"]
+}
+path "kv-v2/metadata/path/" {
+ capabilities = ["list", "read", "delete"]
+}
+path "kv-v2/destroy/path/" {
+ capabilities = ["update"]
+}
+
+path "kv-v2/data/path/userid/*" {
+ capabilities = ["create", "update", "read", "delete", "list"]
+}
+path "kv-v2/delete/path/userid/*" {
+ capabilities = ["update"]
+}
+path "kv-v2/metadata/path/userid/*" {
+ capabilities = ["list", "read", "delete"]
+}
+path "kv-v2/destroy/path/userid/*" {
+ capabilities = ["update"]
+}
+
+# Additional access for UI
+path "kv-v2/metadata" {
+ capabilities = ["list"]
+}
+EOF
+
+##### or #####
+
+vault policy write ui-kv-policy - << EOF
+
+path "kv-v2/data/path/userid" {
+ capabilities = ["create", "update", "read", "delete", "list"]
+}
+path "kv-v2/delete/path/userid" {
+ capabilities = ["update"]
+}
+path "kv-v2/metadata/path/userid" {
+ capabilities = ["list", "read", "delete"]
+}
+path "kv-v2/destroy/path/userid" {
+ capabilities = ["update"]
+}
+
+# Additional access for UI
+path "kv-v2/metadata/*" {
+ capabilities = ["list"]
+}
+EOF
+
+
생성한 Policy 기반으로 인증 생성
$ vault write auth/userpass/users/userid password=password policies=ui-kv-policy
+
vault token create -policy=ui-kv-policy
+
생성한 인증의 Token
을 사용하여 데이터가 조회됨을 확인
$ curl --request GET --header "X-Vault-Token: s.FqFMzKL8ExjJeBrq79Jjh1eB" http://172.28.128.11:8200/v1/kv-v2/data/path/userid | jq .
+{
+ "request_id": "d3db0633-2e13-ba98-4d79-ca48f2307d5e",
+ "lease_id": "",
+ "renewable": false,
+ "lease_duration": 0,
+ "data": {
+ "data": {
+ "access_key": "1234",
+ "secret_key": "1234"
+ },
+ "metadata": {
+ "created_time": "2021-07-16T06:35:51.496079412Z",
+ "deletion_time": "",
+ "destroyed": false,
+ "version": 1
+ }
+ },
+ "wrap_info": null,
+ "warnings": null,
+ "auth": null
+}
+
UI에서 접근가능한지 확인
참고 : 본 글은 AEWS 스터디 7주차 내용중 일부로 작성된 내용입니다.
Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes.
# 설치
+helm repo add argo https://argoproj.github.io/argo-helm
+helm repo update
+helm install argocd argo/argo-cd --set server.service.type=LoadBalancer --namespace argocd --create-namespace --version 5.42.3
+
+# External IP 확인
+EXTERNAL_IP=$(k get svc -n argocd argocd-server -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')
+echo $EXTERNAL_IP
+
+# admin 계정의 암호 확인
+ARGOPW=$(kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d)
+echo $ARGOPW
+mf8bOtNEq7iHMqq1
+
# 최신버전 설치
+curl -sSL -o argocd-linux-amd64 https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
+install -m 555 argocd-linux-amd64 /usr/local/bin/argocd
+chmod +x /usr/local/bin/argocd
+
+# 버전 확인
+argocd version --short
+
+# argocd 서버 로그인
+argocd login $EXTERNAL_IP --username admin --password $ARGOPW
+WARNING: server certificate had error: tls: failed to verify certificate: x509: certificate signed by unknown authority. Proceed insecurely (y/n)? y
+'admin:login' logged in successfully
+Context 'k8s-argocd-argocdse-789cd00c72-b0b60b99b16f1fc7.elb.ap-northeast-2.amazonaws.com' updated
+
+# 기 설치한 깃랩의 프로젝트 URL 을 argocd 깃 리포지토리(argocd repo)로 등록. 깃랩은 프로젝트 단위로 소스 코드를 보관.
+argocd repo add <저장소 주소> --username <계정명> --password <암호>
+
+# 등록 확인 : 기본적으로 아르고시디가 설치된 쿠버네티스 클러스터는 타깃 클러스터로 등록됨
+argocd repo list
+TYPE NAME REPO INSECURE OCI LFS CREDS STATUS MESSAGE PROJECT
+git https://github.com/hyungwook0221/argo-demo.git false false false true Successful
+
+# 기본적으로 아르고시디가 설치된 쿠버네티스 클러스터는 타깃 클러스터로 등록됨
+argocd cluster list
+SERVER NAME VERSION STATUS MESSAGE PROJECT
+https://kubernetes.default.svc in-cluster Unknown Cluster has no applications and is not being monitored.
+
해당 저장소는 개인이 생성한 Git 저장소로 대체하셔도 됩니다.
필자가 만든 저장소를 그대로 사용한다면, "ArgoCD Application CRD" 챕터로 넘어가시면 됩ㄴ디ㅏ.
# Git 저장소 설정
+git clone https://github.com/hyungwook0221/argo-demo.git
+cd argo-demo
+
+# 깃 원격 오리진 주소 확인
+git config -l | grep remote.origin.url
+remote.origin.url=https://github.com/hyungwook0221/argo-demo.git
+
참고 : https://artifacthub.io/packages/helm/bitnami/postgresql
# PostgreSQL 헬름차트 추가 및 다운로드
+helm repo add bitnami https://charts.bitnami.com/bitnami
+helm fetch bitnami/postgresql --untar
+cd postgresql/
+
+# audit.logConnections=false에서 true로 변경
+cat <<EOF > override-values.yaml
+audit:
+ logConnections: true
+EOF
+
+# 헬름 차트를 깃랩 저장소에 업로드
+git add . && git commit -m "add postgresql helm"
+git push
+
# postgresql-helm-argo-application.yml
+---
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name: postgresql-helm
+ namespace: argocd
+spec:
+ destination:
+ namespace: postgresql
+ server: https://kubernetes.default.svc
+ project: default
+ source:
+ repoURL: https://github.com/hyungwook0221/argo-demo.git
+ path: postgresql
+ targetRevision: main
+ helm:
+ valueFiles:
+ - override-values.yaml
+ syncPolicy:
+ syncOptions:
+ - CreateNamespace=true
+ automated:
+ selfHeal: true
+ prune: true
+
# 모니터링 : argocd 웹 화면 보고 있기!
+echo -e "Argocd Web URL = $EXTERNAL_IP"
+
+# 배포
+kubectl apply -f postgresql-helm-argo-application.yml
+
+# YAML 파일을 적용(apply)하여 아르고시디 ‘Application’ CRD를 생성
+kubectl get applications.argoproj.io -n argocd
+NAME SYNC STATUS HEALTH STATUS
+postgresql-helm Synced Healthy
+
+# argocd app 배포 확인
+argocd app get postgresql-helm
+Name: argocd/postgresql-helm
+Project: default
+Server: https://kubernetes.default.svc
+Namespace: postgresql
+URL: https://k8s-argocd-argocdse-789cd00c72-b0b60b99b16f1fc7.elb.ap-northeast-2.amazonaws.com/applications/postgresql-helm
+Repo: https://github.com/hyungwook0221/argo-demo.git
+Target: main
+Path: postgresql
+Helm Values: override-values.yaml
+SyncWindow: Sync Allowed
+Sync Policy: Automated (Prune)
+Sync Status: Synced to main (cf8a47a)
+Health Status: Healthy
+
+GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE
+ Namespace postgresql Succeeded Synced namespace/postgresql created
+ Secret postgresql postgresql-helm Synced secret/postgresql-helm created
+ Service postgresql postgresql-helm-hl Synced Healthy service/postgresql-helm-hl created
+ Service postgresql postgresql-helm Synced Healthy service/postgresql-helm created
+apps StatefulSet postgresql postgresql-helm Synced Healthy statefulset.apps/postgresql-helm created
+
Argo CD에는 다양한 시크릿 관리 도구(HashiCorp Vault, IBM Cloud Secrets Manager, AWS Secrets Manager 등)플러그인을 통해 Kubernetes 리소스에 주입할 수 있도록 지원합니다.
플러그인을 통해 Operator 또는 CRD(Custom Resource Definition)에 의존하지 않고 GitOps와 Argo CD로 시크릿 관리 문제를 해결할 수 있습니다.특히 Secret 뿐만 아니라, deployment, configMap 또는 기타 Kubernetes 리소스에도 사용할 수 있습니다.
필자는 그 중에서 가장 대표적인 시크릿 관리 도구인 HashiCorp Vault 플러그인을 연동하는 방법을 알아보겠습니다.
# 저장소 추가
+helm repo add hashicorp https://helm.releases.hashicorp.com
+
+# 저장소 업데이트
+helm repo update
+
+# 저장소 추가확인
+helm search repo hashicorp/vault
+
+# vault-server-values.yaml
+---
+server:
+ dev:
+ enabled: true
+ devRootToken: "root"
+ logLevel: debug
+
+injector:
+ enabled: "false"
+
+# vault 헬름차트 배포
+helm install vault hashicorp/vault -n vault --create-namespace --values vault-server-values.yaml
+
# shell 접속
+kubectl exec -n vault vault-0 -it -- sh
+
+# enable kv-v2 engine in Vault
+vault secrets enable kv-v2
+
+# create kv-v2 secret with two keys
+vault kv put kv-v2/demo user="secret_user" password="secret_password"
+
+# create policy to enable reading above secret
+vault policy write demo - <<EOF
+path "kv-v2/data/demo" {
+ capabilities = ["read"]
+}
+EOF
+
# enable Kubernetes Auth Method
+vault auth enable kubernetes
+
+# get Kubernetes host address
+# K8S_HOST="https://kubernetes.default.svc"
+# K8S_HOST="https://$(env | grep KUBERNETES_PORT_443_TCP_ADDR| cut -f2 -d'='):443"
+# K8S_HOST="https://$( kubectl exec -n vault vault-0 -- env | grep KUBERNETES_PORT_443_TCP_ADDR| cut -f2 -d'='):443"
+
+# get Service Account token from Vault Pod
+#SA_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
+# SA_TOKEN=$(kubectl exec -n vault vault-0 -- cat /var/run/secrets/kubernetes.io/serviceaccount/token)
+
+# get Service Account CA certificate from Vault Pod
+#SA_CERT=$(cat /var/run/secrets/kubernetes.io/serviceaccount/ca.crt)
+#SA_CERT=$(kubectl exec -n vault vault-0 -- cat /var/run/secrets/kubernetes.io/serviceaccount/ca.crt)
+
+# configure Kubernetes Auth Method
+# kubectl exec -n vault vault-0 -- vault write auth/kubernetes/config \
+# token_reviewer_jwt=$SA_TOKEN \
+# kubernetes_host=$K8S_HOST \
+# kubernetes_ca_cert=$SA_CERT
+
+# 인증방식 업데이트
+vault write auth/kubernetes/config \
+ token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
+ kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443" \
+ kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
+
+# create authenticate Role for ArgoCD
+vault write auth/kubernetes/role/argocd \
+ bound_service_account_names=argocd-repo-server \
+ bound_service_account_namespaces=argocd \
+ policies=demo \
+ ttl=48h
+
+exit
+
💡 참고
kind: Secret
+apiVersion: v1
+metadata:
+ name: argocd-vault-plugin-credentials
+ namespace: argocd
+type: Opaque
+stringData:
+ AVP_AUTH_TYPE: "k8s"
+ AVP_K8S_ROLE: "argocd"
+ AVP_TYPE: "vault"
+ VAULT_ADDR: "http://vault.vault:8200"
+
공식문서를 통해 Argo CD에 Vault Plugin을 설치하는 방법은 크게 4가지 방법 있으며, 크게는 **2가지 방법**으로 구분해서 소개하고 있습니다. 참고
방안1. Installation via a sidecar container (new, starting with Argo CD v2.4.0)
Download AVP and supporting tools into a volume and control everything as Kubernetes manifests, using an off-the-shelf sidecar image
Create a custom sidecar image with AVP and supporting tools pre-installed
방안2. Installation via argocd-cm
ConfigMap (2.6.0에 deprecated 예정)
Download AVP in a volume and control everything as Kubernetes manifests
Create a custom argocd-repo-server
image with AVP and supporting tools pre-installed
필자는 v2.4.0부터 제공되는 사이드카 방식을 통해 구성하는 방법을 채택했습니다.
사이드카 컨테이너에 마운트할 컨피그맵에서 플러그인을 정의
💡 참고 :
apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: cmp-plugin
+ namespace: argocd
+data:
+ avp.yaml: |
+ apiVersion: argoproj.io/v1alpha1
+ kind: ConfigManagementPlugin
+ metadata:
+ name: argocd-vault-plugin
+ spec:
+ allowConcurrency: true
+ discover:
+ find:
+ command:
+ - sh
+ - "-c"
+ - "find . -name '*.yaml' | xargs -I {} grep \"<path\\|avp\\.kubernetes\\.io\" {} | grep ."
+ generate:
+ command:
+ - argocd-vault-plugin
+ - generate
+ - "."
+ lockRepo: false
+ avp-helm.yaml: |
+ ---
+ apiVersion: argoproj.io/v1alpha1
+ kind: ConfigManagementPlugin
+ metadata:
+ name: argocd-vault-plugin-helm
+ spec:
+ allowConcurrency: true
+
+ # Note: this command is run _before_ any Helm templating is done, therefore the logic is to check
+ # if this looks like a Helm chart
+ discover:
+ find:
+ command:
+ - sh
+ - "-c"
+ - "find . -name 'Chart.yaml' && find . -name 'values.yaml'"
+ generate:
+ # **IMPORTANT**: passing `${ARGOCD_ENV_helm_args}` effectively allows users to run arbitrary code in the Argo CD
+ # repo-server (or, if using a sidecar, in the plugin sidecar). Only use this when the users are completely trusted. If
+ # possible, determine which Helm arguments are needed by your users and explicitly pass only those arguments.
+ command:
+ - sh
+ - "-c"
+ - |
+ helm template $ARGOCD_APP_NAME -n $ARGOCD_APP_NAMESPACE ${ARGOCD_ENV_HELM_ARGS} . |
+ argocd-vault-plugin generate -s argocd:argocd-vault-plugin-credentials -
+ lockRepo: false
+ avp-kustomize.yaml: |
+ ---
+ apiVersion: argoproj.io/v1alpha1
+ kind: ConfigManagementPlugin
+ metadata:
+ name: argocd-vault-plugin-kustomize
+ spec:
+ allowConcurrency: true
+
+ # Note: this command is run _before_ anything is done, therefore the logic is to check
+ # if this looks like a Kustomize bundle
+ discover:
+ find:
+ command:
+ - find
+ - "."
+ - -name
+ - kustomization.yaml
+ generate:
+ command:
+ - sh
+ - "-c"
+ - "kustomize build . | argocd-vault-plugin generate -"
+ lockRepo: false
+---
+
argocd-repo-server를 패치하여 argocd-vault-plugin을 다운로드하고 사이드카를 정의하기 위한 initContainer를 추가합니다.
💡 참고 :
apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: argocd-repo-server
+spec:
+ template:
+ spec:
+ automountServiceAccountToken: true
+ volumes:
+ # volumes절 아래 추가
+ - configMap:
+ name: cmp-plugin
+ name: cmp-plugin
+ - name: custom-tools
+ emptyDir: {}
+ initContainers:
+ # initContainers 절 아래 추가
+ # 필자는 편의상 alpine/curl 이미지 사용하여 바이너리 다운로드
+ - name: download-tools
+ image: alpine/curl
+ env:
+ - name: AVP_VERSION
+ value: 1.15.0
+ command: [sh, -c]
+ args:
+ - >-
+ curl -L https://github.com/argoproj-labs/argocd-vault-plugin/releases/download/v$(AVP_VERSION)/argocd-vault-plugin_$(AVP_VERSION)_linux_amd64 -o argocd-vault-plugin &&
+ chmod +x argocd-vault-plugin &&
+ mv argocd-vault-plugin /custom-tools/
+ volumeMounts:
+ - mountPath: /custom-tools
+ name: custom-tools
+ # argocd-vault-plugin 배포방안(3가지 중 선택)
+ containers:
+ # AVP : argocd-vault-plugin with plain YAML
+ - name: avp
+ command: [/var/run/argocd/argocd-cmp-server]
+ image: quay.io/argoproj/argocd:v2.7.4
+ securityContext:
+ runAsNonRoot: true
+ runAsUser: 999
+ volumeMounts:
+ - mountPath: /var/run/argocd
+ name: var-files
+ - mountPath: /home/argocd/cmp-server/plugins
+ name: plugins
+ - mountPath: /tmp
+ name: tmp
+
+ # Register plugins into sidecar
+ - mountPath: /home/argocd/cmp-server/config/plugin.yaml
+ subPath: avp.yaml
+ name: cmp-plugin
+
+ # Important: Mount tools into $PATH
+ - name: custom-tools
+ subPath: argocd-vault-plugin
+ mountPath: /usr/local/bin/argocd-vault-plugin
+
+ # AVP-Helm : argocd-vault-plugin with Helm
+ - name: avp-helm
+ command: [/var/run/argocd/argocd-cmp-server]
+ image: quay.io/argoproj/argocd:v2.7.4
+ securityContext:
+ runAsNonRoot: true
+ runAsUser: 999
+ volumeMounts:
+ - mountPath: /var/run/argocd
+ name: var-files
+ - mountPath: /home/argocd/cmp-server/plugins
+ name: plugins
+ - mountPath: /tmp
+ name: tmp
+
+ # Register plugins into sidecar
+ - mountPath: /home/argocd/cmp-server/config/plugin.yaml
+ subPath: avp-helm.yaml
+ name: cmp-plugin
+
+ # Important: Mount tools into $PATH
+ - name: custom-tools
+ subPath: argocd-vault-plugin
+ mountPath: /usr/local/bin/argocd-vault-plugin
+
+ # AVP-Kustomize : argocd-vault-plugin with Kustomize
+ - name: avp-kustomize
+ command: [/var/run/argocd/argocd-cmp-server]
+ image: quay.io/argoproj/argocd:v2.4.0
+ securityContext:
+ runAsNonRoot: true
+ runAsUser: 999
+ volumeMounts:
+ - mountPath: /var/run/argocd
+ name: var-files
+ - mountPath: /home/argocd/cmp-server/plugins
+ name: plugins
+ - mountPath: /tmp
+ name: tmp
+
+ # Register plugins into sidecar
+ - mountPath: /home/argocd/cmp-server/config/plugin.yaml
+ subPath: avp-kustomize.yaml
+ name: cmp-plugin
+
+ # Important: Mount tools into $PATH
+ - name: custom-tools
+ subPath: argocd-vault-plugin
+ mountPath: /usr/local/bin/argocd-vault-plugin
+
💡 참고
ConfigManagementPlugin
설정을 위한 configMap 생성 - 링크apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: cmp-plugin
+ namespace: argocd
+data:
+ avp-helm.yaml: |
+ apiVersion: argoproj.io/v1alpha1
+ kind: ConfigManagementPlugin
+ metadata:
+ name: argocd-vault-plugin-helm
+ spec:
+ allowConcurrency: true
+ discover:
+ find:
+ command:
+ - sh
+ - "-c"
+ - "find . -name 'Chart.yaml' && find . -name 'values.yaml'"
+ generate:
+ command:
+ - bash
+ - "-c"
+ - |
+ helm template $ARGOCD_APP_NAME --include-crds -n $ARGOCD_APP_NAMESPACE -f ${ARGOCD_ENV_HELM_VALUES} . |
+ argocd-vault-plugin generate -s argocd:argocd-vault-plugin-credentials -
+ lockRepo: false
+
💡 참고
argocd-helm-values.yaml
repoServer:
+ rbac:
+ - verbs:
+ - get
+ - list
+ - watch
+ apiGroups:
+ - ''
+ resources:
+ - secrets
+ - configmaps
+ initContainers:
+ - name: download-tools
+ image: alpine/curl
+ env:
+ - name: AVP_VERSION
+ value: 1.14.0
+ command: [sh, -c]
+ args:
+ - >-
+ curl -L https://github.com/argoproj-labs/argocd-vault-plugin/releases/download/v$(AVP_VERSION)/argocd-vault-plugin_$(AVP_VERSION)_linux_amd64 -o argocd-vault-plugin &&
+ chmod +x argocd-vault-plugin &&
+ mv argocd-vault-plugin /custom-tools/
+ volumeMounts:
+ - mountPath: /custom-tools
+ name: custom-tools
+ extraContainers:
+ - name: avp-helm
+ command: [/var/run/argocd/argocd-cmp-server]
+ image: quay.io/argoproj/argocd:v2.7.4
+ securityContext:
+ runAsNonRoot: true
+ runAsUser: 999
+ volumeMounts:
+ - mountPath: /var/run/argocd
+ name: var-files
+ - mountPath: /home/argocd/cmp-server/plugins
+ name: plugins
+ - mountPath: /tmp
+ name: tmp-dir
+ - mountPath: /home/argocd/cmp-server/config
+ name: cmp-plugin
+ - name: custom-tools
+ subPath: argocd-vault-plugin
+ mountPath: /usr/local/bin/argocd-vault-plugin
+ volumes:
+ - configMap:
+ name: cmp-plugin
+ name: cmp-plugin
+ - name: custom-tools
+ emptyDir: {}
+ - name: tmp-dir
+ emptyDir: {}
+
+# If you face issue with ArgoCD CRDs installation, then uncomment below section to disable it
+#crds:
+# install: false
+
해당 방안의 경우에는 argocd-cm
configMap을 수정하여 적용하는 방안입니다.
💡 참고 : 2.6.0에서 Deprecated 될 예정
containers:
+ - name: argocd-repo-server
+ # volumeMounts절에 custom-tools 추가
+ volumeMounts:
+ - name: custom-tools
+ mountPath: /usr/local/bin/argocd-vault-plugin
+ subPath: argocd-vault-plugin
+ # volume절에 custom-tools 추가
+ volumes:
+ - name: custom-tools
+ emptyDir: {}
+ # init Container 추가
+ initContainers:
+ - name: download-tools
+ image: alpine:3.8
+ command: [sh, -c]
+ # Don't forget to update this to whatever the stable release version is
+ # Note the lack of the `v` prefix unlike the git tag
+ env:
+ - name: AVP_VERSION
+ value: "1.14.0"
+ args:
+ - >-
+ wget -O argocd-vault-plugin
+ https://github.com/argoproj-labs/argocd-vault-plugin/releases/download/v${AVP_VERSION}/argocd-vault-plugin_${AVP_VERSION}_linux_amd64 &&
+ chmod +x argocd-vault-plugin &&
+ mv argocd-vault-plugin /custom-tools/
+ volumeMounts:
+ - mountPath: /custom-tools
+ name: custom-tools
+
💡 참고 : Git 저장소에 대한 Fork 후 진행
# 샘플 애플리케이션 배포를 위한 저장소 추가
+# argocd repo add <저장소 주소> --username <계정명> --password <암호>
+argocd repo add https://github.com/hyungwook0221/spring-boot-debug-app --username <계정명> --password <암호>
+
+# 등록 확인 : 기본적으로 아르고시디가 설치된 쿠버네티스 클러스터는 타깃 클러스터로 등록됨
+argocd repo list
+TYPE NAME REPO INSECURE OCI LFS CREDS STATUS MESSAGE PROJECT
+git https://github.com/hyungwook0221/argo-demo.git false false false true Successful
+git https://github.com/hyungwook0221/spring-boot-debug-app false false false true Successful
+
apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name: demo
+ namespace: argocd
+spec:
+ destination:
+ namespace: argocd
+ server: https://kubernetes.default.svc
+ project: default
+ source:
+ path: infra/helm
+ repoURL: https://github.com/hyungwook0221/spring-boot-debug-app
+ targetRevision: main
+ plugin:
+ env:
+ - name: HELM_ARGS
+ value: '-f override-values.yaml'
+ syncPolicy:
+ automated:
+ prune: true
+ selfHeal: true
+
위 Application 배포시 사용될 override-values.yaml
파일의 코드 중 Vault를 통해서 받아올 부분은 다음과 같습니다.
#(생략)
+envs:
+ - name: VAULT_SECRET_USER
+ value: <path:kv-v2/data/demo#user>
+ - name: VAULT_SECRET_PASSWORD
+ value: <path:kv-v2/data/demo#password>
+
해당 Values 파일에 등록된 VAULT_SECRET_USER
, VAULT_SECRET_PASSWORD
값은 Vault의 KV-V2에 저장된 값을 호출하여 실제 매니페스트로 저장되어 배포될 때에는 다음과 같이 파싱된 후 기입됩니다.
이 외에 추가 데모 시나리오는 다음 글에서 이어서 업로드 하겠습니다!🔥
Vault의 AppRole 인증 방식은 Vault Token을 얻기위한 단기 자격증명을 사용하는 장점이 있지만 자동화된 환경에 어울리는(반대로 사람에게 불편한)방식으로 Vault를 이용하는 애플리케이션/스크립트의 배포 파이프라인을 구성하는 방식을 추천합니다.
$ sw_vers
+ProductName: macOS
+ProductVersion: 12.4
+
+$ brew --version
+Homebrew 3.5.2
+
+$ git version
+git version 2.27.0
+
+$ java -version
+openjdk version "11.0.14.1" 2022-02-08
+
+$ gradle --version
+Welcome to Gradle 7.4.2!
+
+$ docker version
+Client:
+ Version: 20.10.9
+
+Server:
+ Engine:
+ Version: 20.10.14
+
+$ vault version
+Vault v1.11.0
+
+$ nomad version
+Nomad v1.3.1
+
+$ curl --version
+curl 7.79.1 (x86_64-apple-darwin21.0)
+
경고
개발모드로 실행하면 데이터가 메모리에만 저장되어 종료시 삭제 됩니다.
vault server -dev -dev-root-token-id=root
+
-dev-root-token-id
: 개발모드에서는 root 토큰을 지정가능Another terminal
export VAULT_ADDR=http://127.0.0.1:8200 #Vault 주소
+export VAULT_TOKEN=root #Vault Root Token
+export JENKINS_POLICY=jenkins-policy #테스트용 Jenkins Policy 이름
+
정보
테스트를 위한 Secret Engine으로 AWS를 활성화 합니다.
AWS Secret Engine을 사용하기 위해서는 AWS Credential 정보가 필요합니다.
발급 안내 : https://docs.aws.amazon.com/ko_kr/powershell/latest/userguide/pstools-appendix-sign-up.html
export AWS_ACCESS_KEY=AKIAU3NXXXXX
+export AWS_SECRET_KEY=Rex3GPUKO3++123
+export AWS_REGION=ap-northeast-2
+
vault secrets enable -path=aws aws
+
-path
인수로 aws
Secret Engine이 마운트되는 경로를 설정할 수 있고, 기본 endpoint는 aws
vault write aws/config/root \
+ access_key=$AWS_ACCESS_KEY \
+ secret_key=$AWS_SECRET_KEY \
+ region=$AWS_REGION
+
aws
Secret Engine의 기본 설정vault write /aws/config/lease lease=1m lease_max=1m
+
aws
Secret Engine에서 iam_user
형태의 role을 생성하는 경우의 ttllease
는 해당 aws
Secret Engine에서 생성되는 계정의 기본 유지 기간을 설정768h
(32일)vault write aws/roles/sts-s3 \
+ credential_type=federation_token \
+ policy_document=-<<EOF
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:PutObject",
+ "s3:PutObjectAcl"
+ ],
+ "Resource": "*"
+ }
+ ]
+}
+EOF
+
federation_token
(STS) 타입의 role 정의$ vault write aws/sts/sts-s3 ttl=900
+Key Value
+--- -----
+lease_id aws/sts/sts-s3/Qxfoy2plVAfS57tDQ99B5vPM
+lease_duration 14m59s
+lease_renewable false
+access_key ASIAU3NXDWRUE3FCRWBM
+secret_key TX76EXmadilWw3TySTscB1XGAPI4kNyhdQIdKKtS
+security_token IQoJb3JpZ2luX2VjENb//////////wEaDmFwLW5vcnRoZWFzdC0yIkcwRQIhAM
+
iam_user
형태인 경와 다르게 중간에 sts
경로를 넣음ttl
은 federation_token
인 role에서는 15분(900초)가 최소 값앞서 생성한 aws
Secret Engine의 federation_token
을 획득하기 위한 Vault 인증을 추가합니다. AppRole은 기계친화적인 인증방식으로 Username/Password 방식과 빗대어 Username역할을 하는 role_id
와 Password 역할을 하는 secret_id
페어로 인증하게 됩니다. secret_id
는 영구적이지 않으므로 필요할 때 발급받아 사용합니다. 필요할 때 발급받게 되는 것으로 보안성은 높으나 발급받기위한 자동화 구성이 요구됩니다.
cat <<EOF | vault policy write aws_policy -
+path "aws/sts/sts-s3" {
+ capabilities = ["read","update"]
+}
+EOF
+
aws/sts/sts-s3
에 대한 읽기(발급)와 갱신 권한aws_policy
정책을 갖는 AppRole 생성
$ vault auth enable approle
+
+Success! Enabled approle auth method at: approle/
+
+$ vault write auth/approle/role/aws-cred \
+ secret_id_ttl=1m \
+ token_ttl=60m \
+ token_max_ttl=120m \
+ policies="aws_policy"
+
+Success! Data written to: auth/approle/role/aws-cred
+
+$ vault read auth/approle/role/aws-cred/role-id
+
+Key Value
+--- -----
+role_id 430111ee-5955-aa83-a53d-924b7e11ac36
+
+$ vault write -f auth/approle/role/aws-cred/secret-id
+
+Key Value
+--- -----
+secret_id 7f86b671-2f47-f841-18a4-c36ca34ab8d8
+secret_id_accessor 9ad4256a-acc6-e8c0-f7fe-7633e66b1318
+secret_id_ttl 1m
+
생성한 role_id
와 secret_id
로 Vault에 인증을 수행합니다. secret_id
의 경우 ttl 지정이 가능 합니다.
# Test
+$ vault read -field role_id auth/approle/role/aws-cred/role-id > role_id.txt
+$ vault write -f -field secret_id auth/approle/role/aws-cred/secret-id > secret_id.txt
+$ unset VAULT_TOKEN
+$ vault write auth/approle/login role_id=$(cat ./role_id.txt) secret_id=$(cat ./secret_id.txt)
+
+Key Value
+--- -----
+token hvs.CAESIDoJqJmtgUQXj_DSHaSLwkdZFQQpjr7_x-r_bmy6ZbpyGh4KHGh2cy5SeEVpWGpmcXlJSE95WEpKUVdKSW8zMXI
+token_accessor SpMtPdNUXaF3GAOATtWD0Qdi
+token_duration 1h
+token_renewable true
+token_policies ["aws_policy" "default"]
+identity_policies []
+policies ["aws_policy" "default"]
+token_meta_role_name aws-cred
+
+$ vault read /aws/creds/s3
+
+Key Value
+--- -----
+lease_id aws/creds/s3/1iaII6N6DaUULD27w91ZpePP
+lease_duration 1m
+lease_renewable true
+access_key AKIAU3NXDWRUEXSGRTNL
+secret_key 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
+security_token <nil>
+
role_id
는 Pipeline 작성에 사용Jenkins에서는 생성된 AppRole의 role_id
에 대한 secret_id
를 발급받을 권한이 있어야 합니다.
cat <<EOF | vault policy write approle_policy -
+path "auth/approle/role/aws-cred/role-id" {
+ capabilities = ["read"]
+}
+path "auth/approle/role/aws-cred/secret-id" {
+ capabilities = ["create", "update"]
+}
+EOF
+
role-id
를 읽기 가능secret-id
를 생성 가능Jenkins는 Token을 넣어줄 것이므로 Token Role을 생성하여 Entity를 하나로 지정합니다.
Role을 생성하지 않는 경우 일반적인 vault token create -policy=<policy_name> -orphan=true -period=700h
같은 명령어로도 생성할 수 있습니다.
# Get token accessor id
+$ vault auth list -format=json | jq -r '.["token/"].accessor' > accessor_token.txt
+
+# Create entity for Jenkins
+$ vault write -format=json identity/entity name="jenkins-entity" policies="default" \
+ metadata=organization="Company" \
+ metadata=team="Security" \
+ | jq -r ".data.id" > entity_id.txt
+
+# Create alias for Jenkins
+$ vault write identity/entity-alias name="jenkins-alias" \
+ canonical_id=$(cat entity_id.txt) \
+ mount_accessor=$(cat accessor_token.txt) \
+ custom_metadata=account="Security Account"
+
+Key Value
+--- -----
+canonical_id 4a950f94-3c35-dc0f-cdf3-4f5bc931ae0b
+id 66528d3b-1561-283b-d7d2-33c8b683b49f
+
+# Create role for Jenkins
+$ vault write auth/token/roles/jenkins allowed_policies="approle_policy" orphan=false bound_cidrs="127.0.0.1/32,128.252.0.0/16" renewable=true allowed_entity_aliases="jenkins-alias" token_period="720h"
+
+Success! Data written to: auth/token/roles/jenkins
+
+# Create Token
+$ vault token create -field=token -entity-alias=jenkins-alias -role=jenkins
+hvs.CAESIGkayX80rrAdHR-LychPp6GITGM_DG8Af8VpOY36hdHQGh4KHGh2cy5pNXprSDdOMEVMTEtia0QxYW1YRHF4dmo
+
token
은 Jenkins에 저장하는 값# Test
+$ vault token create -field=token -entity-alias=jenkins-alias -role=jenkins > token.txt
+$ VAULT_TOKEN=$(cat ./token.txt) vault write -f -field secret_id auth/approle/role/aws-cred/secret-id
+
+aae55515-f9ed-a171-dd56-53710ab29018
+
Nomad는 CI/CD 파이프라인 구조 상 배포를 위한 대상을 생성하기위해 구성되었습니다.
nomad agent -dev -alloc-dir=/tmp/nomad/alloc -state-dir=/tmp/nomad/state
+
Another terminal
export NOMAD_ADDR=http://127.0.0.1:4646
+
java 드라이버를 사용한 배포에서 빌드된 jar파일을 원격에서 불러와야 하므로 임시 파일 서버를 구성합니다.
cat <<EOF | nomad job run -
+job "fileserver" {
+ datacenters = ["dc1"]
+
+ group "fileserver" {
+ count = 1
+
+ network {
+ port "http" {
+ to = 3000
+ static = 3000
+ }
+ }
+
+ task "fileserver" {
+ driver = "docker"
+
+ config {
+ image = "julienmeerschart/simple-file-upload-download-server"
+ ports = ["http"]
+ }
+ }
+ }
+}
+EOF
+
Upload Test
$ curl -F file=@/tmp/dynamic.properties http://localhost:3000
+{"downloadLink":"http://localhost:3000/file?file=dynamic.properties","curl":"curl http://localhost:3000/file?file=dynamic.properties > dynamic.properties"}
+
Download Guide : https://www.jenkins.io/download/
macOS Install Guide : https://www.jenkins.io/download/lts/macos/
macOS guide : https://www.jenkins.io/download/lts/macos/
brew install jenkins-lts
+
brew services start jenkins-lts
+
Home 디렉토리의 Jenkins 활성화를 위한 패스워드를 다음 경로에서 복사하여 http://localhost:8080 페이지의 Unlock Jenkins
에 입력
빠른 시작을 위해 기본 값인 Install suggested plugins
를 클릭
계정명, 암호, 이름, 이메일 주소를 기입하고 Save and Continue
버튼 클릭
올바른 Jenkins URL을 확인하고 Save and Finish
버튼 클릭
Start using Jenkins
버튼 클릭
GitHub 로그인
우측 상단 사용자 메뉴 클릭 후 Settings
클릭
좌측 메뉴 최하단 Developer settings
클릭
좌측 메뉴 Personal access tokens
클릭
Generate new token
버튼 클릭
Token 옵션 선택 후 Generate token
클릭
생성된 토큰을 기록/보관
Jenkins 관리
> 시스템 설정
으로 이동
JDK
항목에서 Add JDK
클릭
Git
항목에서 Add Git
을 클릭
GitHub
항목에서 Add GitHub Server
드롭박스의 GitHub Server
를 클릭
+Add
버튼 클릭하여 Jenkins
선택 후 새로운 크리덴셜 생성 후 생성된 항목 지정Secret Test
선택Test Connection
버튼으로 연결 확인Gradle
항목에서 Add Gradle
클릭
Name : 이름 입력 (e.g. gradle)
GRADLE_HOME : Gradle 홈 디렉토리 입력 (e.g. /usr/local/Cellar/gradle/7.4.2/libexec)
Jenkins 관리
> Manage Credentials
로 이동Add credentials
클릭Kind : Secret text
Scope : Global
ID : 이름 (e.g. vault)
Secret: jenkins role 의 토큰
$ vault token create -field=token -entity-alias=jenkins-alias -role=jenkins
+hvs.CAESIGkayX80rrAdHR-LychPp6GITGM_DG8Af8VpOY36hdHQGh4KHGh2cy5pNXprSDdOMEVMTEtia0QxYW1YRHF4dmo
+
github : https://github.com/Great-Stone/jenkins-gradle-vault-pipeline
build.gradle
<생략>
+dependencies {
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+ implementation 'org.springframework.cloud:spring-cloud-starter-vault-config'
+ implementation 'org.springframework.cloud:spring-cloud-vault-config-aws'
+ testImplementation 'org.springframework.boot:spring-boot-starter-test'
+}
+<생략>
+
demo>src>main>resources>application.yml
server:
+ port: ${NOMAD_HOST_PORT_http:8080}
+spring:
+ config:
+ import: vault://
+ cloud.vault:
+ enabled: true
+ host: ${VAULT_HOST:127.0.0.1}
+ port: ${VAULT_PORT:8200}
+ scheme: http
+ uri: ${VAULT_URI:http://127.0.0.1:8200/}
+ config:
+ lifecycle:
+ min-renewal: 14m
+ expiry-threshold: 15m
+ authentication: APPROLE
+ app-role:
+ role-id: ${VAULT_ROLE_ID:430111ee-5955-aa83-a53d-924b7e11ac36}
+ secret-id: ${VAULT_SECRET_ID:6db07578-b019-95b4-6741-de4c79cbde39}
+ role: aws-cred
+ app-role-path: approle
+ kv:
+ enabled: false
+ aws:
+ enabled: true
+ role: sts-s3
+ backend: aws
+ credential-type: federation_token
+ access-key-property: cloud.aws.credentials.accessKey
+ secret-key-property: cloud.aws.credentials.secretKey
+ session-token-key-property: cloud.aws.credentials.sessionToken
+ ttl: 900s
+
$ export VAULT_TOKEN=root
+$ export VAULT_ROLE_ID=$(vault read -field role_id auth/approle/role/aws-cred/role-id)
+$ export VAULT_SECRET_ID=$(write -f -field secret_id auth/approle/role/aws-cred/secret-id)
+$ gradle bootRun
+...
+2022-06-28 23:23:22.721 INFO 35144 --- [ scheduling-1] com.example.demo.VaultAWSConfiguration : AwsConfigurationProperties [accessKey=ASIAU3NXDWRUABR7V6UM, secretKey=IBXXM3FXq8Q7qQE2XVUnjPN5lcN8bvsf5bw3TwXX, sessionToken=tokentoken]
+2022-06-28 23:23:24.721 INFO 35144 --- [ scheduling-1] com.example.demo.VaultAWSConfiguration : AwsConfigurationProperties [accessKey=ASIAU3NXDWRUABR7V6UM, secretKey=IBXXM3FXq8Q7qQE2XVUnjPN5lcN8bvsf5bw3TwXX, sessionToken=tokentoken]
+<==========---> 80% EXECUTING [5s]
+> :bootRun
+
Pipeline 구성
- GitHub checkout
- Gradle build
- jar upload
- Nomad Job Start
좌측 + 새로운 Item
버튼 클릭
이름 입력 (e.g. Nomad Job - Vault approve)
Pipeline 선택 후 OK
성성된 Jenkins Job의 Pipeline에 스크립트 구성
pipeline {
+ agent any
+ triggers {
+ cron('H */8 * * *') //regular builds
+ pollSCM('* * * * *') //polling for changes, here once a minute
+ }
+ tools {
+ git('local')
+ gradle('gradle')
+ jdk("jdk11")
+ }
+ environment {
+ NOMAD_ADDR = 'http://localhost:4646'
+ VAULT_HOST = '127.0.0.1'
+ VAULT_PORT = '8200'
+ }
+ stages {
+ stage('Clone') {
+ steps {
+ git branch: 'main',
+ credentialsId: 'jenkins_github',
+ url: 'https://github.com/Great-Stone/jenkins-gradle-vault-pipeline'
+ sh "ls -lat"
+ }
+ }
+ stage('Test') {
+ steps {
+ sh './gradlew test'
+ }
+ }
+ stage('Build') {
+ steps {
+ sh './gradlew build'
+ }
+ }
+ stage('Upload') {
+ steps {
+ sh 'mv ./build/libs/demo-0.0.1-SNAPSHOT.jar ./demo-vault-${BUILD_NUMBER}.jar'
+ sh 'curl -F file=@./demo-vault-${BUILD_NUMBER}.jar http://localhost:3000'
+ }
+ }
+ stage('Nomad Download') {
+ steps {
+ sh 'curl -C - --output nomad_1.3.1_darwin_amd64.zip https://releases.hashicorp.com/nomad/1.3.1/nomad_1.3.1_darwin_amd64.zip'
+ sh 'unzip -o nomad_1.3.1_darwin_amd64.zip'
+ }
+ }
+ stage('Deploy To Nomad') {
+ steps {
+ script {
+ withCredentials([string(credentialsId: 'vault', variable: 'TOKEN')]) {
+ sh '''
+ curl -H "X-Vault-Token: ${TOKEN}" -X GET http://${VAULT_HOST}:${VAULT_PORT}/v1/auth/approle/role/aws-cred/role-id | /usr/local/bin/jq -r '.data.role_id' > role_id.txt
+ curl -H "X-Vault-Token: ${TOKEN}" -X POST http://${VAULT_HOST}:${VAULT_PORT}/v1/auth/approle/role/aws-cred/secret-id | /usr/local/bin/jq -r '.data.secret_id' > secret_id.txt
+ ./nomad job run -var version=${BUILD_NUMBER} -var vault_host=${VAULT_HOST} -var vault_port=${VAULT_PORT} -var role_id=$(cat ./role_id.txt) -var secret_id=$(cat ./secret_id.txt) ./nomad-java.hcl
+ '''
+ }
+ }
+ }
+ }
+ }
+}
+
지금 빌드
를 클릭하여 빌드를 진행합니다.
jenkins와 vault otp를 연동하여 pipe line에서 ssh/scp test
# ssh 권한을 사용 할 policy 생성
+$ tee ssh-policy.hcl <<EOF
+# To list SSH secrets paths
+path "ssh/*" {
+ capabilities = [ "list" ]
+}
+# To use the configured SSH secrets engine otp_key_role role
+path "ssh/creds/otp_key_role" {
+ capabilities = ["create", "read", "update"]
+}
+EOF
+
+#ssh(otp) 정책 생성
+$ vault policy write ssh ssh-policy.hcl
+
+#rest api에서 사용 할 token 생성
+$ vault token create -policy=ssh
+
// jenkins pipe line v1
+pipeline {
+ agent any
+ environment {
+ // 위에서 생성한 credential id
+ ssh_token = credentials('vault_ssh_token')
+ }
+ options {
+ buildDiscarder(logRotator(numToKeepStr: '20'))
+ disableConcurrentBuilds()
+ }
+ stages{
+ stage('SSH') {
+ steps{
+ // 1. curl로 받아 온 password를 변수에 담음
+ // 2. ssh에 자동으로 패스워드를 입력하기 위해 sshpass 명령어 추가 사용
+ // -o StrictHostKeyChecking=no는 최초 로그인에 known_hosts에 등록하는 문구 무시
+ // scp도 동일하게 사용 가능
+ // 주의할점은 다음라인은 jenkins 서버로 돌아온다.
+ sh '''
+ ssh_passwd=$(curl --header "X-Vault-Token: $ssh_token" --request POST --data '{"ip": "172.21.2.56"}' http://172.21.2.50:8200/v1/ssh/creds/otp_key_role | jq ".data.key" | tr -d '""')
+ sshpass -p $ssh_passwd ssh ubuntu@172.21.2.56 -o StrictHostKeyChecking=no "cd /usr/local \
+ && ls -la \
+ && pwd"
+
+ ssh_passwd=$(curl --header "X-Vault-Token: $ssh_token" --request POST --data '{"ip": "172.21.2.56"}' http://172.21.2.50:8200/v1/ssh/creds/otp_key_role | jq ".data.key" | tr -d '""')
+ sshpass -p $ssh_passwd scp -o StrictHostKeyChecking=no ~/a ubuntu@172.21.2.56:~/test
+ '''
+ }
+ }
+ }
+}
+
jenkins와 vault를 연동하여 pipe line에서 kv 사용하기
이 예제는 진짜 kv까지만 테스트함
# approle 엔진 생성
+$ vault auth enable approle
+# kv2 enable
+$ vault secrets enable kv-v2
+# kv enable
+$ vault secrets enable -path=kv kv
+
+# jenkins 정책으로 될 파일 생성 v1, v2
+$ tee jenkins-policy.hcl <<EOF
+path "kv/secret/data/jenkins/*" {
+ capabilities = [ "read" ]
+}
+path "kv-v2/data/jenkins/*" {
+ capabilities = [ "read" ]
+}
+EOF
+
+#jenkins 정책 생성
+vault policy write jenkins jenkins-policy.hcl
+
+#approle 생성 및 정책 jenkins에 연결
+vault write auth/approle/role/jenkins token_policies="jenkins" \
+token_ttl=1h token_max_ttl=4h
+
+#Role id, secret id 가져오기
+
+vault read auth/approle/role/jenkins/role-id
+vault write -f auth/approle/role/jenkins/secret-id
+
+
+vault secrets enable -path=kv kv
+$ tee gitlab.json <<EOF
+{
+ "gitlabIP": "172.21.2.52",
+ "api-key": "RjLAbbWsSAzXoyBvo2qL"
+}
+EOF
+
+tee gitlab-v2.json <<EOF
+{
+ "gitlabIP": "172.21.2.52",
+ "api-key": "RjLAbbWsSAzXoyBvo2qL",
+ "version": "v2"
+}
+EOF
+
+vault kv put kv/secret/data/jenkins/gitlab @gitlab.json
+vault kv put kv-v2/jenkins/gitlab @gitlab-v2.json
+
# jenkins pipe line v1
+def secrets = [
+ [path: 'kv%2Fsecret/data/jenkins/gitlab', engineVersion:1, secretValues: [
+ [envVar: 'gitlabIP', vaultKey: 'gitlabIP'],
+ [envVar: 'API_KEY', vaultKey: 'api-key']]],
+]
+def configuration = [vaultUrl: 'http://172.21.2.50:8200', vaultCredentialId: 'vault-approle', engineVersion: 1]
+
+pipeline {
+ agent any
+ options {
+ buildDiscarder(logRotator(numToKeepStr: '20'))
+ disableConcurrentBuilds()
+ }
+ stages{
+ stage('Vault') {
+ steps {
+ withVault([configuration: configuration, vaultSecrets: secrets]) {
+ sh "echo $gitlabIP"
+ sh "echo ${env.API_KEY}"
+ sh "curl -v $gitlabIP"
+ }
+ }
+ }
+ }
+}
+
# jenkins pipe line v2
+def secrets = [
+ [path: 'kv-v2/jenkins/gitlab', engineVersion:2, secretValues: [
+ [envVar: 'gitlabIP', vaultKey: 'gitlabIP'],
+ [envVar: 'API_KEY', vaultKey: 'api-key'],
+ [envVar: 'version2', vaultKey: 'version']]]
+]
+
+def configuration = [vaultUrl: 'http://172.21.2.50:8200', vaultCredentialId: 'vault-approle', engineVersion: 2]
+
+pipeline {
+ agent any
+ options {
+ buildDiscarder(logRotator(numToKeepStr: '20'))
+ disableConcurrentBuilds()
+ }
+ stages{
+ stage('Vault') {
+ steps {
+ withVault([configuration: configuration, vaultSecrets: secrets]) {
+ sh "echo ${env.API_KEY}"
+ sh "echo ${env.version2}"
+ sh "curl -v ${env.gitlabIP}"
+ }
+ }
+ }
+ }
+}
+
Demo App Github : https://github.com/Great-Stone/vault-mtls-demo
SSL(Secure Sokets Layer, 보안 소캣 계층)는 클라이언트와 서버 사이에 전송된 데이터를 암호화 하고 인터넷 연결에 보안을 유지하는 표준 기술이다. 악의적 외부인이 클라이언트와 서버 사이에 전송되는 정보를 확인 및 탈취하는 것을 방지한다.
TLS(Transport Layer Security, 전송 계층 보안)는 현재 더이상 사용되지 않는 SSL을 계승하는 보다 진보된 보안 기술이다. SSL 3.0을 기반으로 만들어졌지만 호환되지는 않는다. 최신 버전은 TLS 1.3이다.
SSL 기술이 TLS로 대체되었다고 하지만 여전히 브라우저 인증서는 SSL 인증서라고 불린다.
TLS에서는 서버에만 TLS 인증서 및 공개/개인 키 쌍이 있고 클라이언트에는 없다. TLS 프로세스는 다음과 같다.
mTLS에서는 클라이언트와 서버 모두에 인증서가 있고 양쪽에서 공개/개인 키 쌍을 사용하여 인증한다. TLS 대비 mTLS는 양쪽을 확인하기 위한 추가 단계가 있다.
먼저 mTLS의 장점을 살펴보면,
mTLS의 단점은 다음과 같다.
from flask import Flask, render_template, request, make_response
+import ssl
+
+app = Flask(__name__)
+
+### APIs ###
+
+if __name__ == "__main__":
+ app.debug = True
+ ssl_context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH, cafile='ca.crt')
+ ssl_context.load_cert_chain(certfile=f'site.crt', keyfile=f'site.key', password='')
+ ssl_context.verify_mode = ssl.CERT_REQUIRED
+ app.run(host="0.0.0.0", port=src_port, ssl_context=ssl_context, use_reloader=True)
+
# default.conf
+server {
+ listen 443 ssl;
+
+ access_log /var/log/nginx/access.log;
+ error_log /var/log/nginx/error.log;
+
+ ssl_certificate /etc/ssl/server.crt;
+ ssl_certificate_key /etc/ssl/server.key;
+ ssl_protocols TLSv1.2 TLSv1.3;
+ ssl_client_certificate /etc/nginx/client_certs/ca.crt;
+ ssl_verify_client on;
+ ssl_verify_depth 2;
+
+ location / {
+ if ($ssl_client_verify != SUCCESS) { return 403; }
+
+ ### 구성 ###
+ }
+}
+
<VirtualHost *:80>
+ ServerName {DOMAIN}
+ Redirect permanent / https://{DOMAIN}
+</VirtualHost>
+
+<IfModule mod_ssl.c>
+ <VirtualHost *:443>
+ ServerAdmin info@{DOMAIN}
+ ServerName {DOMAIN}
+
+ Header always set Strict-Transport-Security "max-age=63072000; includeSubdomains;"
+
+ SSLEngine on
+ SSLCompression Off
+ SSLProtocol ALL -SSLv2 -SSLv3
+ SSLHonorCipherOrder On
+ SSLCipherSuite EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH
+ SSLCertificateFile {SSL}/fullchain.pem
+ SSLCertificateKeyFile {SSL}/privkey.pem
+ SSLCACertificateFile {PATH}/ca.crt
+ SSLStrictSNIVHostCheck on
+
+ <Location / >
+ SSLVerifyClient require
+ SSLVerifyDepth 1
+
+ Options FollowSymLinks
+ AllowOverride None
+ </Location>
+
+ <Location /health>
+ SSLVerifyClient none
+ </Location>
+
+ ProxyPreserveHost On
+ ProxyRequests off
+ ProxyPass / http://localhost/
+ ProxyPassReverse / http://localhost/
+ </VirtualHost>
+</IfModule>
+
볼트가 제공하는 PKI 기능과 Agent의 자동 교체 기능을 활용하여 인증서 관리와 발급을 자동화하여 애플리케이션과 서버에 대한 부담을 줄이고 mTLS의 장점을 취할 수 있다.
- 참고 : https://bitgadak.tistory.com/5
- openssl 대신 smallstep 을 사용하면 좀더 간단하다 : https://smallstep.com/hello-mtls/doc/client/requests
- socket example : https://www.electricmonk.nl/log/2018/06/02/ssl-tls-client-certificate-verification-with-python-v3-4-sslcontext/
OpenSSL을 활용하여 볼트를 사용하지 않고 mTLS를 구현하는 과정을 설명한다.
root ca 생성을 위한 root key를 생성한다.
cd cert
+openssl genrsa -out root.key 2048
+
OS에 따라(Linux/MacOS) 권한 변경이 권장된다.
chmod 600 root.key
+
생성된 root.key
기반의 root ca 인증서 생성을 위한 요청서를 생성한다.
$ openssl req -config ca.conf -extensions usr_cert -new -key root.key -out ca.csr
+
-config
: 미리 구성해 놓은 ca용 구성 정보를 읽는다.openssl-xxx.conf
sample
구분 | 작성 예 |
---|---|
Country Name (국가코드) | KR |
State or Province Name (시/도의 전체이름) | Seoul |
Locality Name (시/군/구 등의 이름) | Songpa-gu |
Organization (회사이름) | XXXX |
Organization Unit (부서명) | Server |
Common Name (SSL 인증서를 설치할 서버의 Full Domain) | www.xxxx.com |
$ openssl req -text -in ca.csr
+Certificate Request:
+ Data:
+ Version: 0 (0x0)
+ Subject: C=KR, ST=Seoul, L=Seoul, O=COMPANY, OU=DEV/emailAddress=example@example.com, CN=example root
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ RSA Public-Key: (2048 bit)
+ Modulus:
+ <...생략...>
+ Exponent: 65537 (0x10001)
+ Attributes:
+ Requested Extensions:
+ X509v3 Basic Constraints:
+ CA:TRUE
+ Signature Algorithm: sha256WithRSAEncryption
+ <...생략...>
+-----BEGIN CERTIFICATE REQUEST-----
+<...생략...>
+-----END CERTIFICATE REQUEST-----
+
생성된 요청서에 대해 자체 서명(self-signning)한다.
openssl x509 -req -days 3650 -in ca.csr -signkey root.key -extfile ca.ext -out ca.crt
+
-days
: 인증서 기간은 10년으로 하였다.-extfile
: 서명시 추가 정보에 대한 내용을 읽는다.$ openssl x509 -text -noout -in ca.crt
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number:
+ ee:38:a2:de:5e:b2:11:c8
+ Signature Algorithm: sha256WithRSAEncryption
+ Issuer: C=KR, ST=Seoul, L=Seoul, O=COMPANY, OU=DEV/emailAddress=example@example.com, CN=example root
+ Validity
+ Not Before: Mar 15 03:04:58 2023 GMT
+ Not After : Mar 12 03:04:58 2033 GMT
+ Subject: C=KR, ST=Seoul, L=Seoul, O=COMPANY, OU=DEV/emailAddress=example@example.com, CN=example root
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ RSA Public-Key: (2048 bit)
+ Modulus:
+ <...생략...>
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Basic Constraints:
+ CA:TRUE
+ Signature Algorithm: sha256WithRSAEncryption
+ <...생략...>
+
생성된 root ca 파일을 시스템에 신뢰할 수 있는 인증서로 등록하면, 브라우저 호출시 신뢰할 수 없는 인증서로 인한 경고 창이 뜨지 않는다.
ca.crt
를 더블클릭하여 키체인 접근
앱에 인증서
탭에 등록하고, 등록된 example.com
인증서를 더블클릭하여 신뢰
항목에서 이 인증서 사용 시
를 항상 신뢰
로 변경한다./etc/pki/ca-trust/source/anchors/
에 인증서를 복사 한 후, update-ca-trust
명령을 실행한다.ca.crt
를 더블클릭하여 인증서 창의 인증서 설치...
를 클릭, 인증서 가져오기 마법사
로 신뢰할 수 있는 인증서로 등록한다.데모 서비스 A용 인증서를 생성하기 위해 해당 인증서를 위한 key를 생성한다. 생성 시 패스워드를 넣어주며, 패스워드 없는 key를 생성하려는 경우 한번더 풀어주는 과정이 필요하다.
# 패스워드 4자리 이상 입력
+openssl genrsa -aes256 -out service-a-with-pw.key 2048
+# 패스워드 없는 key
+openssl rsa -in service-a-with-pw.key -out service-a.key
+
서비스 A용 인증서를 위한 요청서를 생성한다.
openssl req -new -key service-a.key -config service-a.conf -out service-a.csr
+
-config
: 미리 구성해 놓은 서비스 A용 구성 정보를 읽는다.자체 서명과정에서 앞서 생성한 root ca 인증서와 key를 넣어 서비스 A인증서가 root ca에 종속되도록 구성한다.
openssl x509 -req -days 365 -in service-a.csr -extfile service-a.ext -CA ca.crt -CAkey root.key -CAcreateserial -out service-a.crt
+$ openssl x509 -text -in service-a.crt
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number:
+ ec:71:b0:dd:72:c2:a2:4a
+ Signature Algorithm: sha256WithRSAEncryption
+ Issuer: C=KR, ST=Seoul, L=Seoul, O=COMPANY, OU=DEV/emailAddress=example@example.com, CN=example root
+ Validity
+ Not Before: Mar 15 03:36:06 2023 GMT
+ Not After : Mar 14 03:36:06 2024 GMT
+ Subject: C=KR, ST=Seoul, L=Seoul, O=COMPANY, OU=DEV/emailAddress=example@example.com, CN=service-a.example.com
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ RSA Public-Key: (2048 bit)
+ Modulus:
+ <..생략..>
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Subject Alternative Name:
+ DNS:service-a.example.com
+ Signature Algorithm: sha256WithRSAEncryption
+ <..생략..>
+-----BEGIN CERTIFICATE-----
+<..생략..>
+-----END CERTIFICATE-----
+
-days
: 인증서 기간을 1년으로 하였다.-CA
: root ca 인증서를 지정한다.-CAkey
: root ca의 key를 지정한다.-CAcreateserial
: 서명 작업에 root ca가 인증서에 대한 일련번호 생성-extfile
: 서비스 A를 위한 추가 정보서비스 B에 대한 인증서도 생성한다. 앞서 설명된 내용을 생략하고 아래 커맨드만 나열한다.
openssl genrsa -aes256 -out service-b-with-pw.key 2048
+
+openssl rsa -in service-b-with-pw.key -out service-b.key
+
+openssl req -new -key service-b.key -config service-b.conf -out service-b.csr
+
+openssl x509 -req -days 365 -in service-b.csr -extfile service-b.ext -CA ca.crt -CAkey root.key -CAcreateserial -out service-b.crt
+
데모 앱은 Python으로 구성되었다.
$ python --version
+Python 3.10.5
+
+$ pip --version
+pip 23.0.1
+
+$ pip install requests flask
+
127.0.0.1 service-a.example.com service-b.example.com
+
cd python_service_a
+python main.py
+
cd python_service_b
+python main.py
+
Python으로 작성된 flask api server 구성은 다음과 같다.
# main.py
+
+### 생략 ###
+if __name__ == "__main__":
+ app.debug = True
+ ssl_context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH, cafile='../cert/ca.crt')
+ ssl_context.load_cert_chain(certfile=f'../cert/{src}.crt', keyfile=f'../cert/{src}.key', password='')
+ # ssl_context.verify_mode = ssl.CERT_REQUIRED
+ app.run(host="0.0.0.0", port=src_port, ssl_context=ssl_context, use_reloader=True, extra_files=[f'../cert/{src}.crt'])
+
ssl.create_default_context
: flask에서 사용할 ssl context를 정의한다. 여기 cafile
에 root ca 파일을 지정한다.ssl_context.load_cert_chain
: cert와 key를 지정하여 인증서 체인을 설정한다.ssl_context.verify_mode
: service A는 인증서 검증을 무시할 수 있도록 해당 옵션에 주석처리 한다.app.run(..., extra_files=[f'../cert/{src}.crt'])
: 인증서가 변경되면 flask를 다시 시작하도록 구성한다.서비스 A의 경우 https로 접근할 수 있고, ssl.CERT_REQUIRED
옵션이 활성화 되어있지 않아 신뢰할 수 없는 인증서라도 curl로 --insecure
옵션을 추가하여 응답을 확인할 수 있다. 브라우저에서도 별도의 신뢰 확인을 통해 접근가능하다.
$ curl https://service-a.example.com:7443
+
+curl: (60) SSL certificate problem: self signed certificate in certificate chain
+More details here: https://curl.se/docs/sslcerts.html
+
+curl failed to verify the legitimacy of the server and therefore could not
+establish a secure connection to it. To learn more about this situation and
+how to fix it, please visit the web page mentioned above.
+$ curl --insecure https://service-a.example.com:7443
+
+Hello from "service-a"%
+
Python으로 작성된 flask api server 구성은 다음과 같다.
# main.py
+
+### 생략 ###
+if __name__ == "__main__":
+ app.debug = True
+ ssl_context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH, cafile='../cert/ca.crt')
+ ssl_context.load_cert_chain(certfile=f'../cert/{src}.crt', keyfile=f'../cert/{src}.key', password='')
+ ssl_context.verify_mode = ssl.CERT_REQUIRED
+ app.run(host="0.0.0.0", port=src_port, ssl_context=ssl_context, use_reloader=True, extra_files=[f'../cert/{src}.crt'])
+
ssl_context.verify_mode = ssl.CERT_REQUIRED
설정으로 인해 인증서 검증이 반드시 필요하도록 설정한다.--insecure
옵션을 추가하더라도 서비스 B는 인증서를 요구한다.
$ curl --insecure https://service-b.example.com:8443
+curl: (56) LibreSSL SSL_read: error:1404C45C:SSL routines:ST_OK:reason(1116), errno 0
+
따라서 요청시 root ca, cert(인증서), key를 함께 사용해야 한다.
$ curl --cacert ca.crt --key service-b.key --cert service-b.crt https://service-b.example.com:8443
+
서비스 A에서 B로 요청할 때 인증서 모두를 설정한 경우이다. 응답이 정상적으로 오는지 확인한다.
https://service-a.example.com:7443/w-mtls
서비스 A에서 B로 요청할 때 A의 인증정보를 담지 않은 경우이다. 서비스 B에서 인증서를 요구하는 메시지가 출력된다.
https://service-a.example.com:7443/wo-cert-mtls
# 응답
+SSLError(1, '[SSL: TLSV13_ALERT_CERTIFICATE_REQUIRED] tlsv13 alert certificate required')
+
서비스 A에서 B로 요청할 때 root ca를 포함하지 않는 경우이다. 인증을 위한 자체 서명 인증서를 요구한다.
https://service-a.example.com:7443/wo-ca-mtls
# 응답
+SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate in certificate chain')
+
faketime : https://github.com/wolfcw/libfaketime
faketime
을 사용하여 서비스 A의 인증서 만료 기간을 현재시간 이전으로 만든다.
faketime '2023-01-01 00:00:00' /bin/bash -c 'openssl x509 -req -days 30 -in service-a.csr -extfile service-a.ext -CA ca.crt -CAkey root.key -CAcreateserial -out service-a.crt'
+
서비스 A가 보유한 인증서가 만료된 경우 인증서 만료됨을 표기한다. (서비스 B 인증서는 정상)
https://service-a.example.com:7443/w-mtls
# 응답
+SSLError(SSLError(1, '[SSL: SSLV3_ALERT_CERTIFICATE_EXPIRED] sslv3 alert certificate expired')
+
faketime : https://github.com/wolfcw/libfaketime
faketime
을 사용하여 서비스 B의 인증서 만료 기간을 현재시간 이전으로 만든다.
faketime '2023-01-01 00:00:00' /bin/bash -c 'openssl x509 -req -days 30 -in service-b.csr -extfile service-b.ext -CA ca.crt -CAkey root.key -CAcreateserial -out service-b.crt'
+
서비스 B가 보유한 인증서가 만료된 경우 인증서 만료됨을 표기한다. (서비스 A 인증서는 정상)
https://service-a.example.com:7443/w-mtls
SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired')
+
A와 B의 인증서 Root CA가 다른 경우 인증서 서명이 다르므로 요청 실패한다. 아래와 같이 서비스 B를 위한 인증서를 root ca부터 새로 생성한다.
cd cert
+
+openssl genrsa -out root-b.key 2048
+
+chmod 600 root-b.key
+
+openssl req -config ca.conf -extensions usr_cert -new -key root-b.key -out ca-b.csr
+
+openssl x509 -req -days 3650 -in ca-b.csr -signkey root-b.key -extfile ca-b.ext -out ca-b.crt
+
+openssl genrsa -aes256 -out service-b-with-pw.key 2048
+
+openssl rsa -in service-b-with-pw.key -out service-b.key
+
+openssl req -new -key service-b.key -config service-b.conf -out service-b.csr
+
+openssl x509 -req -days 365 -in service-b.csr -extfile service-b.ext -CA ca-b.crt -CAkey root-b.key -CAcreateserial -out service-b.crt
+
python_service_b
의 main.py
에 서 ca 파일 이름을 변경한다.
if __name__ == "__main__":
+ app.debug = True
+ ssl_context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH, cafile='../cert/ca-b.crt')
+ ssl_context.load_cert_chain(certfile=f'../cert/{src}.crt', keyfile=f'../cert/{src}.key', password='')
+ ssl_context.verify_mode = ssl.CERT_REQUIRED
+ app.run(host="0.0.0.0", port=src_port, ssl_context=ssl_context, use_reloader=True, extra_files=[f'../cert/{src}.crt'])
+
요청 시 서비스 A와 B의 서명이 달라 오류가 발생함을 확인한다.
https://service-a.example.com:7443/w-mtls
SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate signature failure')
+
테스트가 끝났으면 다시 root ca 파일을 원래의 같은 ca.crt
파일로 지정한다.
Vault Download : https://releases.hashicorp.com/vault/
Vault의 인증서 관리 및 자동화 관리 방안을 설명한다.
vault server -dev -dev-root-token-id=root
+
export VAULT_ADDR='http://127.0.0.1:8200'
+$ vault login
+
+Token (will be hidden): root
+
vault secrets enable pki
+
Vault 기본 Max TTL
은 32일(786h) 이므로 원하는 TTL
로 변경한다.
vault secrets tune -max-lease-ttl=87600h pki
+
vault write pki/root/generate/internal \
+ key_bits=2048 \
+ private_key_format=pem \
+ signature_bits=256 \
+ country=KR \
+ province=Seoul \
+ locality=KR \
+ organization=COMPANY \
+ ou=DEV \
+ common_name=example.com \
+ ttl=87600h
+
Certificate Revocation List(인증서 해지 목록) 엔드포인트 작성
vault write pki/config/urls \
+ issuing_certificates="http://127.0.0.1:8200/v1/pki/ca" \
+ crl_distribution_points="http://127.0.0.1:8200/v1/pki/crl"
+
미리 Role을 구성해 놓으면 사용자 및 앱은 지정된 규칙에 따라 인증서를 발급받을 수 있다.
vault write pki/roles/example-dot-com \
+ allowed_domains=example.com \
+ allow_subdomains=true \
+ max_ttl=72h
+
vault write pki/issue/example-dot-com \
+ common_name=service-a.example.com
+
vault_agent
디렉토리에서 작업한다.
Vault Agent는 볼트가 가지고 있는 시크릿 정보를 발급 및 TTL
만료 시 자동 갱신해주는 역할을 수행한다.
Vault Agent가 취득할 정책을 추가한다. 앞서 생성한 PKI 시크릿 엔진에 대한 권한이 설정되어있다.
$ vault policy write pki pki_policy.hcl
+
$ vault auth enable approle
+Success! Enabled approle auth method at: approle/
+
+$ vault write auth/approle/role/pki-agent \
+ secret_id_ttl=120m \
+ token_ttl=60m \
+ token_max_tll=120m \
+ policies="pki"
+Success! Data written to: auth/approle/role/pki-agent
+
+$ vault read auth/approle/role/pki-agent/role-id
+Key Value
+--- -----
+role_id dfa2a248-1e1b-e2e9-200c-69c63b9ca447
+
+$ vault write -f auth/approle/role/pki-agent/secret-id
+Key Value
+--- -----
+secret_id 864360c1-c79f-ea7c-727b-7752361fe1ba
+secret_id_accessor 3cc068e2-a172-2bb1-c097-b777c3525ba6
+
Vault Agent 실행 시 approle 인증방식을 사용하도록 구성하는 예제로, role_id
와 secret_id
가 필요하다. Vault Agent 재기동시에는 secret_id
를 재발급 해야 한다.
$ vault read -field=role_id auth/approle/role/pki-agent/role-id > roleid
+
+$ vault write -f -field=secret_id auth/approle/role/pki-agent/secret-id > secretid
+
Vault Agent는 Template에 따라 시크릿을 특정 파일로 랜더링하는 기능을 갖고 있다.
# ca-a.tpl
+{{- /* ca-a.tpl */ -}}
+{{ with secret "pki/issue/example-dot-com" "common_name=service-a.example.com" "ttl=2m" }}
+{{ .Data.issuing_ca }}{{ end }}
+
위 구문은 pki/issue/example-dot-com
에서 common_name=service-a.example.com
인 인증서를 발급하는 것으로, 테스트를 위해 ttl=2m
로 짧게 설정하였다. 볼트로 부터 받는 결과 중에서 issuing_ca
값을 랜더링한다.
vault_agent.hcl
에서는 위 Template에 대한 랜더링 결과를 특정 파일로 저장하도록 명시한다.
template {
+ source = "ca-a.tpl"
+ destination = "../cert/ca.crt"
+}
+
vault agent -config=vault_agent.hcl -log-level=debug
+
지정된 TTL
간격마다 템플릿 랜더링 로그 확인한다.
...
+2023-03-18T22:29:09.312+0900 [DEBUG] (runner) rendering "ca-a.tpl" => "../cert/ca.crt"
+2023-03-18T22:29:09.312+0900 [DEBUG] (runner) checking template a04612e63b9a03a45ef968a8984a23db
+2023-03-18T22:29:09.312+0900 [DEBUG] (runner) rendering "cert-a.tpl" => "../cert/service-a.crt"
+2023-03-18T22:29:09.312+0900 [DEBUG] (runner) checking template 850589d81f7afe64c7c5a0a8440c8569
+2023-03-18T22:29:09.312+0900 [DEBUG] (runner) rendering "key-a.tpl" => "../cert/service-a.key"
+2023-03-18T22:29:09.312+0900 [DEBUG] (runner) checking template 60e7f2683d2c76a501eb54879bf89ad2
+2023-03-18T22:29:09.312+0900 [DEBUG] (runner) rendering "cert-b.tpl" => "../cert/service-b.crt"
+2023-03-18T22:29:09.333+0900 [INFO] (runner) rendered "cert-b.tpl" => "../cert/service-b.crt"
+2023-03-18T22:29:09.333+0900 [DEBUG] (runner) checking template 1fb22b9f15857b7eeb0b68a3c9ac6d20
+2023-03-18T22:29:09.334+0900 [DEBUG] (runner) rendering "key-b.tpl" => "../cert/service-b.key"
+2023-03-18T22:29:09.354+0900 [INFO] (runner) rendered "key-b.tpl" => "../cert/service-b.key"
+
랜더링이 완료되고, 파일이 갱신되면 Python의 Flask 설정의 extra_files
항목이 변경되므로 재시작되어 인증서를 다시 읽어온다.
* Detected change in '/vault-examples/mtls-pki/cert/130906523', reloading
+ * Detected change in '/vault-examples/mtls-pki/cert/service-a.crt', reloading
+ * Restarting with watchdog (fsevents)
+ * Debugger is active!
+ * Debugger PIN: 136-647-438
+
변경된 인증서를 확인해보면 갱신된 유효기간을 확인할 수 있고, 브라우저에서도 인증서 보기를 통해 변경된 인증서의 유효기간을 확인할 수 있다.
인증서 같은 시크릿은 파일 형태로 관리되는데, 이런 파일이 변경되면 애플리케이션 또는 웹서버나 솔루션에서 감지하는 구성이 필요하다. 데모 앱인 Python의 Flask에서는 Debug모드에 extra_files
에 인증서를 지정하여 변경되는 인증서를 감지하도록 하였으나 이는 운영에서는 권장되지 않는 방식이며 인증서 교체와 함께 watch
, reload
, restart
에 대한 동작이 요구된다.
애플리케이션에서 내부적으로 코드 구현을 통해 이를 교체하는 방법도 있으나, mTLS가 적용되는 코드 전반에 변경이 필요하므로 HasihCorp Nomad같은 Vault 연계된 애플리케이션 오케스트레이터를 활용할 수 있다.
Vault의 인증서 관리 및 자동화 관리 방안을 Nomad와 연계하여 설명한다.
Nomad Download : https://releases.hashicorp.com/nomad/
준비된 Policy 및 Job은 nomad
디렉토리에 있다.
Nomad 에 부여할 Vault의 정책을 생성한다.
vault policy write nomad-server nomad_policy.hcl
+
Nomad 에서 사용할 Token Role을 생성한다. Nomad에서 허용되는 정책은 앞서 생성한 pki
이다.
vault write auth/token/roles/nomad-cluster allowed_policies="pki" disallowed_policies=nomad-server token_explicit_max_ttl=0 orphan=true token_period="259200" renewable=true
+
생성한 Token Role 기반으로 Nomad와의 설정에 사용할 Token을 하나 발급한다.
vault token create -field token -policy nomad-server -period 72h -orphan > /tmp/token.txt
+
Nomad를 실행한다.
$ nomad agent -dev -vault-enabled=true -vault-address=http://127.0.0.1:8200 -vault-token=$(cat /tmp/token.txt) -vault-tls-skip-verify=true -vault-create-from-role=nomad-cluster
+
+==> No configuration files loaded
+==> Starting Nomad agent...
+==> Nomad agent configuration:
+
+ Advertise Addrs: HTTP: 127.0.0.1:4646; RPC: 127.0.0.1:4647; Serf: 127.0.0.1:4648
+ Bind Addrs: HTTP: [127.0.0.1:4646]; RPC: 127.0.0.1:4647; Serf: 127.0.0.1:4648
+ Client: true
+ Log Level: DEBUG
+ Region: global (DC: dc1)
+ Server: true
+ Version: 1.5.1
+
+==> Nomad agent started! Log data will stream in below:
+...
+ 2023-03-19T15:34:30.081+0900 [DEBUG] nomad.vault: starting renewal loop: creation_ttl=72h0m0s
+ 2023-03-19T15:34:30.082+0900 [DEBUG] nomad.vault: successfully renewed server token
+ 2023-03-19T15:34:30.082+0900 [INFO] nomad.vault: successfully renewed token: next_renewal=35h59m59.999944054s
+...
+
export NOMAD_ADDR='http://127.0.0.1:4646'
+
Nomad Job을 해석하면 다음과 같다.
job "mtls-service-a" {
+ datacenters = ["dc1"]
+
+ type = "service"
+
+ group "service" {
+ count = 1
+
+ network {
+ port "https" {
+ static = 7433
+ }
+ }
+
+ # vault에서 할당받을 Polocy를 명시 한다.
+ # 해당 Policy로 생성되는 Token의 변경시 동작은 change_mode에서 지정한다.
+ vault {
+ namespace = ""
+ policies = ["pki"]
+ change_mode = "noop"
+ }
+
+ task "python-task" {
+ driver = "raw_exec"
+
+ config {
+ command = "local/start.sh"
+ }
+ template {
+ data = <<EOH
+#!/bin/bash
+cp -R /Users/gs/workspaces/hashicorp_example/vault-examples/mtls-pki/python_service_a python_service_a
+cd python_service_a
+pip install requests flask
+python main.py
+ EOH
+ destination = "local/start.sh"
+ }
+
+ # Vault Agent에서 구성했던 Template이 Job내에 정의된다.
+ template {
+ data = <<EOH
+{{- /* ca-a.tpl */ -}}
+{{ with secret "pki/issue/example-dot-com" "common_name=service-a.example.com" "ttl=2m" }}
+{{ .Data.issuing_ca }}{{ end }}
+ EOH
+ destination = "/cert/ca.crt"
+ change_mode = "noop"
+ }
+ # 인증서가 변경되는 경우 change_mode에 지정된 restart를 통해 Job을 재시작한다.
+ template {
+ data = <<EOH
+{{- /* cert-a.tpl */ -}}
+{{ with secret "pki/issue/example-dot-com" "common_name=service-a.example.com" "ttl=2m" }}
+{{ .Data.certificate }}{{ end }}
+ EOH
+ destination = "/cert/service-a.crt"
+ change_mode = "restart"
+ }
+ template {
+ data = <<EOH
+{{- /* key-a.tpl */ -}}
+{{ with secret "pki/issue/example-dot-com" "common_name=service-a.example.com" "ttl=2m" }}
+{{ .Data.private_key }}{{ end }}
+ EOH
+ destination = "/cert/service-a.key"
+ change_mode = "noop"
+ }
+ }
+ }
+}
+
change_mode
의 경우 인증서 변경후 동작을 정의하는데,
noop
은 아무 동작도 수행하지 않음을 의미한다.restart
는 Job을 재시작한다.signal
은 system signal을 호출하며, systemctl로 실행되는 프로세스의 경우 SIGHUP
을 지정하면 reload 동작이 발생한다.앞서 Python을 직접 실행했던것과 같이 Nomad 를 통해 Python을 실행하며, 조건은 동일하다. Flask에서 파일 체크를 위해 추가했던 extra_files
는 삭제해도 된다.
nomad job run service_a_job.hcl
+nomad job run service_b_job.hcl
+
Vault 에서 가져온 인증서가 변경되면 change_mode
에 정의된 restart
에 의해 애플리케이션을 자동 재시작 한다.
Consul에서는 mTLS를 위한 인증서를 각 애플리케이션에서 분리하여 envoy로 구현된 proxy에서 이를 대체한다. 따라서 애플리케이션에는 별도 mTLS 구현이 불필요하며, 인증서 교체를 Consul이 제공하는 proxy가 담당하게 된다.
Consul Service Mesh에서 기본 제공하는 mTLS를 사용하는 경우 장점은
단점은 Consul의 Control Plane과 Data Plane을 구분하는 동작으로 인해 추가적인 리소스가 발생한다는 점이다.
Dev Mode 를 활용한 테스트
목적 : Spring boot 기반 애플리케이션에서 Nomad 를 이용하여 Vault의 dynamic secret 을 최소한의 코드변경으로 사용할 수 있는 워크 플로우 구성
코드 기반 인경우의 예제 : https://dev.to/aws-builders/aws-sts-with-spring-cloud-vault-1e5g
Vault-Nomad Integration : https://www.nomadproject.io/docs/integrations/vault-integration
Version (Download)
- Nomad v1.3.1 (2b054e38e91af964d1235faa98c286ca3f527e56)
- Vault v1.10.3 (af866591ee60485f05d6e32dd63dde93df686dfb)
Kubernetes 환경인 경우 Vault CSI Provider를 통해 비슷한 구성 가능 : https://www.vaultproject.io/docs/platform/k8s/csi
vault server -dev -dev-root-token-id=root
+
Another terminal
export VAULT_ADDR=http://127.0.0.1:8200
+export VAULT_TOKEN=root
+export NOMAD_POLICY=nomad-server
+
cat <<EOF | vault policy write $NOMAD_POLICY -
+# Allow creating tokens under "nomad-cluster" token role. The token role name
+# should be updated if "nomad-cluster" is not used.
+path "auth/token/create/nomad-cluster" {
+ capabilities = ["update"]
+}
+
+# Allow looking up "nomad-cluster" token role. The token role name should be
+# updated if "nomad-cluster" is not used.
+path "auth/token/roles/nomad-cluster" {
+ capabilities = ["read"]
+}
+
+# Allow looking up the token passed to Nomad to validate # the token has the
+# proper capabilities. This is provided by the "default" policy.
+path "auth/token/lookup-self" {
+ capabilities = ["read"]
+}
+
+# Allow looking up incoming tokens to validate they have permissions to access
+# the tokens they are requesting. This is only required if
+# `allow_unauthenticated` is set to false.
+path "auth/token/lookup" {
+ capabilities = ["update"]
+}
+
+# Allow revoking tokens that should no longer exist. This allows revoking
+# tokens for dead tasks.
+path "auth/token/revoke-accessor" {
+ capabilities = ["update"]
+}
+
+# Allow checking the capabilities of our own token. This is used to validate the
+# token upon startup.
+path "sys/capabilities-self" {
+ capabilities = ["update"]
+}
+
+# Allow our own token to be renewed.
+path "auth/token/renew-self" {
+ capabilities = ["update"]
+}
+EOF
+
cat <<EOF | vault policy write aws_policy -
+path "aws/sts/s3" {
+ capabilities = ["read","update"]
+}
+EOF
+
+cat <<EOF | vault policy write db_policy -
+path "db/creds/mysql" {
+ capabilities = ["read","update"]
+}
+EOF
+
vault write auth/token/roles/nomad-cluster allowed_policies="aws_policy,db_policy" disallowed_policies="$NOMAD_POLICY" token_explicit_max_ttl=0 orphan=true token_period="259200" renewable=true
+
vault token create -field token -policy $NOMAD_POLICY -period 72h -orphan > /tmp/token.txt
+# vault token create -field token -role nomad-cluster -period 72h -orphan > /tmp/token.txt
+
Docker 이미지 실행을 위해서는 Nomad 실행 환경에 Docker가 설치되어야 합니다.
Java 실행을 위해서는 Nomad 실행 환경에 Java가 설치되어야 합니다.
$ docker version
+Client:
+ Version: 20.10.9
+ API version: 1.41
+ ...
+
+Server:
+ Engine:
+ Version: 20.10.14
+ API version: 1.41 (minimum version 1.12)
+ ...
+
+$ java -version
+openjdk version "11.0.14.1" 2022-02-08
+OpenJDK Runtime Environment Temurin-11.0.14.1+1 (build 11.0.14.1+1)
+OpenJDK 64-Bit Server VM Temurin-11.0.14.1+1 (build 11.0.14.1+1, mixed mode)
+
nomad agent -dev -vault-enabled=true -vault-address=http://127.0.0.1:8200 -vault-token=$(cat /tmp/token.txt) -vault-tls-skip-verify=true -vault-create-from-role=nomad-cluster
+
Another terminal
export NOMAD_ADDR=http://127.0.0.1:4646
+
cat <<EOF | nomad job run -
+job "mysql" {
+ datacenters = ["dc1"]
+
+ type = "service"
+
+ group "mysql-group" {
+ count = 1
+
+ network {
+ port "db" {
+ to = 3306
+ static = 3306
+ }
+ }
+
+ task "mysql-task" {
+ driver = "docker"
+
+ config {
+ image = "mysql:5"
+ ports = ["db"]
+ }
+
+ env {
+ MYSQL_ROOT_PASSWORD = "rooooot"
+ }
+ }
+ }
+}
+EOF
+
$ export AWS_ACCESS_KEY=AKIAU3NXDWRUFZSXYRNX
+$ export AWS_SECRET_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+$ export AWS_REGION=ap-northeast-2
+
+$ vault secrets enable aws
+
+$ vault write aws/config/root \
+ access_key=$AWS_ACCESS_KEY \
+ secret_key=$AWS_SECRET_KEY \
+ region=$AWS_REGION
+
+$ vault write aws/roles/s3 \
+ credential_type=federation_token \
+ policy_document=-<<EOF
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:PutObject",
+ "s3:PutObjectAcl"
+ ],
+ "Resource": "*"
+ }
+ ]
+}
+EOF
+
+$ vault write aws/sts/s3 ttl=15m
+
+Key Value
+--- -----
+lease_id aws/sts/s3/lasSraK69Ii19tUIzI9yXLnR
+lease_duration 14m59s
+lease_renewable false
+access_key ASIAU3NXDWRUOZPCWIGY
+secret_key FXWXK2xHlBbsHhepuZN2yuN5C8kd7qi2PKyMVf+t
+security_token IQoJb3JpZ2luX2VjEND//////////wEaDmFwLW5vcnRoZWFzdC0y
+
$ vault secrets enable -path=db database
+
+$ vault write db/config/my-mysql-database \
+ plugin_name=mysql-database-plugin \
+ connection_url="{{username}}:{{password}}@tcp(127.0.0.1:3306)/" \
+ allowed_roles="mysql" \
+ username="root" \
+ password="rooooot"
+
+$ vault write db/roles/mysql \
+ db_name=my-mysql-database \
+ creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}';GRANT SELECT ON *.* TO '{{name}}'@'%';" \
+ default_ttl="5s" \
+ max_ttl="10s"
+
+$ vault read db/creds/mysql
+
+Key Value
+--- -----
+lease_id db/creds/mysql/VuufZZP1NO9thZj4pPnNtPdU
+lease_duration 10s
+lease_renewable true
+password WkFTPwWPrCe3yeWQoS--
+username v-token-mysql-Cy7p0vP6uOYnW7csKz
+
curl -G https://start.spring.io/starter.zip \
+ -d type=maven-build
+ -d dependencies=web \
+ -d javaVersion=11 \
+ -o demo.zip
+
demo>src>main>resources>application.yml
dynamic:
+ path: ${DYNAMIC_PROPERTIES_PATH:/tmp/dynamic.properties}
+server:
+ port: ${NOMAD_HOST_PORT_http:8080}
+
demo>src>main>java>com>example>demo>DemoApplication.java
package com.example.demo;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.List;
+
+@RestController
+@SpringBootApplication
+@EnableScheduling
+public class DemoApplication {
+
+ private static String FILE_PATH;
+
+ @Value("${dynamic.path}")
+ public void setKey(String value) {
+ FILE_PATH = value;
+ }
+
+ private boolean flag = true;
+
+ public static void main(String[] args) {
+ SpringApplication.run(DemoApplication.class, args);
+ }
+
+ @Scheduled(fixedRate=1000)
+ public void filecheck() throws IOException {
+ List<String> str = Files.readAllLines(Paths.get(FILE_PATH));
+ System.out.println(str);
+ }
+
+ @RequestMapping(method = RequestMethod.GET, path = "/")
+ public String index() throws IOException {
+ List<String> str = Files.readAllLines(Paths.get(FILE_PATH));
+ System.out.println(str);
+
+ return "<h1>AWS</h1>"
+ .concat("<h2>" + str.get(0) + "</h2>")
+ .concat("<h2>" + str.get(1) + "</h2>")
+ .concat("<h2>" + str.get(2) + "</h2>")
+ .concat("<br>")
+ .concat("<h1>MySQL</h1>")
+ .concat("<h2>" + str.get(3) + "</h2>")
+ .concat("<h2>" + str.get(4) + "</h2>");
+ }
+}
+
cat <<EOF> /tmp/dynamic.properties
+aws_access_key=abc
+aws_secret_key=def
+aws_secret_token=ghi
+db_username=user
+db_password=pw
+EOF
+
$ mvn spring-boot:run
+...
+[aws_access_key=abc, aws_secret_key=def, aws_secret_token=ghi, db_username=user, db_password=pw]
+
cat <<EOF> /tmp/dynamic.properties
+aws_access_key=123
+aws_secret_key=456
+aws_secret_token=789
+db_username=user1
+db_password=pw2
+EOF
+
$ mvn install
+...
+[INFO] Building jar: /Users/gs/Downloads/demo/target/demo-0.0.1-SNAPSHOT.jar
+...
+
$ cat <<EOF> Dockerfile
+FROM amazoncorretto:11
+ARG JAR_FILE=target/demo-0.0.1-SNAPSHOT.jar
+COPY ${JAR_FILE} app.jar
+ENV JAVA_OPTS=""
+CMD java $JAVA_OPTS -server -jar app.jar
+EOF
+
$ docker build -t java/vault .
+Step 1/5 : FROM amazoncorretto:11
+11: Pulling from library/amazoncorretto
+8de5b65bd171: Pull complete
+6d24904f7237: Pull complete
+Digest: sha256:34810d3d08456f7e658747d47aec5afc052fcfb2dcadf25db80a51f63086532d
+Status: Downloaded newer image for amazoncorretto:11
+ ---> 299f114f2f6b
+Step 2/5 : ARG JAR_FILE=target/demo-0.0.1-SNAPSHOT.jar
+ ---> Running in 5a0662c5b4a5
+Removing intermediate container 5a0662c5b4a5
+ ---> 608c348e23ac
+Step 3/5 : COPY ${JAR_FILE} app.jar
+ ---> 36d147070bd3
+Step 4/5 : ENV JAVA_OPTS=""
+ ---> Running in 58cb66bb0eab
+Removing intermediate container 58cb66bb0eab
+ ---> f92b3ffeac4d
+Step 5/5 : CMD java $JAVA_OPTS -server -jar app.jar
+ ---> Running in a5d4d1071697
+Removing intermediate container a5d4d1071697
+ ---> 67ae9829dc07
+Successfully built 67ae9829dc07
+Successfully tagged java/vault:latest
+
Nomad Job 명세의 template
을 활용하여 Nomad와 연계된 Vault의 시크릿을 작성 할 수 있음
File (파일)
Env (환경변수) : env
설정이 true
인경우
Nomad Job에서는 앞서 Vault에서 선언한 nomad-cluster
token role에서 정의한 Policy만을 사용할 수 있음
change_mode
값이 기본 restart
이므로 aws와 db 크리덴셜 같이 ttl 이 적용되는 경우 만료시 자동 갱신되기 때문에 파일과 환경변구 갱신만을 하기 위해서는 noop
으로 설정 필요
$ cat <<EOF | nomad job run -
+job "java-test" {
+ datacenters = ["dc1"]
+
+ type = "service"
+
+ group "java" {
+ count = 1
+
+ network {
+ port "http" {} # random port
+ }
+
+ vault {
+ namespace = ""
+ policies = ["aws_policy","db_policy"]
+ change_mode = "noop"
+ }
+
+ task "java-task" {
+ driver = "java"
+
+ config {
+ jar_path = "/demo/target/demo-0.0.1-SNAPSHOT.jar"
+ }
+ env {
+ DYNAMIC_PROPERTIES_PATH = "local/dynamic.properties"
+ }
+ template {
+ data = <<EOH
+{{- with secret "aws/sts/s3" "ttl=15m" }}
+aws_access_key={{ .Data.access_key | toJSON }}
+aws_secret_key={{ .Data.secret_key | toJSON }}
+aws_secret_token={{ .Data.security_token | toJSON }}
+{{- end }}
+{{- with secret "db/creds/mysql" }}
+db_username={{ .Data.username | toJSON }}
+db_password={{ .Data.password | toJSON }}
+{{- end }}
+ EOH
+ env = true
+ destination = "local/dynamic.properties"
+ change_mode = "noop"
+ }
+ }
+ }
+}
+EOF
+
경고
Nomad Dev 모드에서는 파일시스템 접근권한이 없으므로 Prod 모드 구성 필요
$ cat <<EOF | nomad job run -
+job "docker-test" {
+ datacenters = ["dc1"]
+
+ type = "service"
+
+ group "docker" {
+ count = 1
+
+ network {
+ port "http" {}
+ }
+
+ vault {
+ namespace = ""
+ policies = ["aws_policy","db_policy"]
+ change_mode = "noop"
+ }
+
+ task "docker-task" {
+ driver = "docker"
+
+ config {
+ image = "hahohh/java-vault-nomad-demo:0.0.1"
+ ports = ["http"]
+ volumes = [
+ "local:/tmp",
+ ]
+ # auth {
+ # username = "registry username"
+ # password = "registry password"
+ # }
+ }
+ env {
+ DYNAMIC_PROPERTIES_PATH = "/local/dynamic.txt"
+ }
+ template {
+ data = <<EOH
+{{- with secret "aws/sts/s3" "ttl=15m" }}
+aws_access_key={{ .Data.access_key | toJSON }}
+aws_secret_key={{ .Data.secret_key | toJSON }}
+aws_secret_token={{ .Data.security_token | toJSON }}
+{{- end }}
+{{- with secret "db/creds/mysql" }}
+db_username={{ .Data.username | toJSON }}
+db_password={{ .Data.password | toJSON }}
+{{- end }}
+ EOH
+ env = true
+ destination = "local/dynamic.txt"
+ change_mode = "noop"
+ }
+ }
+ }
+}
+EOF
+
Enterprise 기능
Token Role에 bound_cidr
을 적용하거나 여타 인증(AppRole, Userpass 등)에 허용하는 cidr을 적용하는 경우 다시 Token을 발급하거나 인증받지 않는한은 cidr을 기반으로한 차단을 동적으로 적용할 수 없다.
이경우 Sentinel을 사용하여 동적인 정책을 적용할 수 있다. Sentinel은 ACL방식의 기존 Policy
와는 달리 Path가 아닌 다른 검증 조건을 추가할 수 있다.
예제 (GitHub) : https://github.com/hashicorp/vault-guides/blob/master/governance/sentinel/README.md
엔터프라이즈 Trial 신청 (30일) : https://www.hashicorp.com/products/vault/trial
Sentinel 적용을 확인하기 위해 모든 권한이 있는 기존 Policy
방식의 정책을 생성한다.
vault policy write super-user - << EOF
+path "*" {
+capabilities = ["create", "read", "update", "delete", "list", "sudo"]
+}
+EOF
+
생성한 정책고 앞으로 생성할 Sentinel 정책이 포함된 사용자를 생성한다.
vault write auth/userpass/users/admin password=password policies="super-user, test-rgp"
+vault write auth/userpass/users/rgp password=password policies="super-user, test-rgp"
+
admin
과 rgp
사용자 모두 동일한 정책을 부여 받았다. Sentinel에서는 identity
정보를 기반으로 조건을 부여할 수 있으며, 동일한 정책이 부여되었더라도 어떤 identity
인가에 따라 적용 여부를 선택적으로 검증할 수 있다.
각 사용자로 로그인하여 Token 정보를 확인하면 entity_id
값을 확인할 수 있다.
$ TOKEN=$(vault login -field=token -method userpass username=admin password=password)
+
+$ vault token lookup $TOKEN
+
+Key Value
+--- -----
+display_name userpass-admin
+entity_id 17230158-d0ad-dd6d-b749-3c7de9e2b4cf
+policies [default super-user test-rgp]
+renewable true
+ttl 768h
+
다음과 같이 적용할 identity-cidr-check.sentinel
파일을 생성한다. (확장자는 다른 확장자를 사용해도 무방하다. e.g. hcl)
import "sockaddr"
+import "strings"
+
+print(identity.entity.id)
+print(request.connection.remote_addr)
+
+precond = rule {
+ # admin user
+ # identity.entity.id is "17230158-d0ad-dd6d-b749-3c7de9e2b4cf" or
+ # rgp user
+ identity.entity.id is "31cc28c0-9fd0-82b3-a70d-0eef741c5349"
+}
+
+cidrcheck = rule {
+ ## Loopback
+ # sockaddr.is_contained("127.0.0.0/8", request.connection.remote_addr) or
+ sockaddr.is_contained("22.32.4.0/24", request.connection.remote_addr)
+}
+
+main = rule when precond {
+ cidrcheck
+}
+
identity.entity.id
로 검증할 아이디 내용에는 앞서 확인한 admin
과 rgp
사용자의 entity_id
를 조건에 넣는다.admin
사용자의 경우 우선 주석처리하여 진행한다.적용하는 방식은 다음과 같다.
POLICY=$(base64 identity-cidr-check.sentinel)
+
+vault write sys/policies/rgp/test-rgp \
+ policy="${POLICY}" \
+ enforcement_level="hard-mandatory"
+
admin
사용자로 로그인하여 kv를 생성하고 값을 넣는다.
$ vault login -method userpass username=admin password=password
+$ vault secrets enable kv
+$ vault kv put kv/hello foo=bar
+$ vault kv get kv/hello
+=== Data ===
+Key Value
+--- -----
+foo bar
+
rgp
사용자로 로그인하여 kv를 조회해본다.
$ vault login -method userpass username=rgp password=password
+$ vault kv get kv/hello
+Error making API request.
+
+URL: GET http://127.0.0.1:8200/v1/sys/internal/ui/mounts/kv/hello
+Code: 400. Errors:
+
+* 2 errors occurred:
+ * rgp standard policy "root/test-rgp" evaluation resulted in denial.
+
+The specific error was:
+<nil>
+
+A trace of the execution for policy "root/test-rgp" is available:
+
+Result: false
+
+Description: <none>
+
+print() output:
+
+31cc28c0-9fd0-82b3-a70d-0eef741c5349
+127.0.0.1
+
+
+Rule "main" (root/test-rgp:19:1) = false
+Rule "cidrcheck" (root/test-rgp:14:1) = false
+Rule "precond" (root/test-rgp:7:1) = true
+ * permission denied
+
cidrcheck 에서 검증하는 cidr에 속하지 못하면 요청 단계에서 권한이 없음을 표기한다.
앞서 작성한 sentinel 규칙에서 admin
사용자의 identity.id
의 주석을 해제하여 다시 적용해 본다.
...생략...
+
+precond = rule {
+ # admin user
+ identity.entity.id is "17230158-d0ad-dd6d-b749-3c7de9e2b4cf" or
+ # rgp user
+ identity.entity.id is "31cc28c0-9fd0-82b3-a70d-0eef741c5349"
+}
+
+...생략...
+
admin
사용자로 로그인하여 kv를 조회해도 cidr 조건에 맞지 않으면 동일한 오류가 발생한다.
허용하는 cidr을 추가해본다. 로컬에서 테스트하는 경우 127.0.0.1
이 해당 ip가 될 수 있다.
...생략...
+
+cidrcheck = rule {
+ ## Loopback
+ sockaddr.is_contained("127.0.0.0/8", request.connection.remote_addr) or
+ sockaddr.is_contained("22.32.4.0/24", request.connection.remote_addr)
+}
+
+...생략...
+
적용 후 admin
사용자와 rgp
사용자 모두 정상적으로 kv의 값을 확인할 수 있다.
Example Source : https://github.com/Great-Stone/vault_springboot_example
볼트는 애플리케이션(앱)의 구성관리, 특히 사용자 ID, 패스워드, Token, 인증서, 엔드포인트, AWS 자격증명 등과 같은 민감한 정보를 안전하게 저장하는 중앙 집중식 인프라를 제공한다. 서비스의 성장과 더불어, 이를 구성하는 앱은 확장과 분리 요구 사항이 발생하면 구성 관리가 어려워 진다. 특히, 시크릿 정보가 포함되는 구성 관리는 수동으로 관리하는 경우 로컬 환경을 포함한 여러 시스템에 노출되는 위험성을 갖고, 환경마다 다른 시크릿을 관리하기위한 유지 관리의 노력과 비용이 증가한다. 볼트에서 이야기하는 앱과 관련한 "시크릿 스프롤(퍼짐)" 현상은 다음과 같다.
본질적으로 시크릿 스프롤은 가시성과 통제력의 저하를 야기한다.
앱과 구성 관계에서 구성관리의 원칙은 다음과 같다.
볼트는 구성 요소에 대해 중앙 저장소를 제공하며 다음과 같은 주요 이점이 있다.
앱을 위한 볼트 구성을 위해 다음과 같이 볼트를 실행한다.
$ vault server -dev -dev-root-token-id=root -log-level=trace
+
+...
+You may need to set the following environment variables:
+
+ $ export VAULT_ADDR='http://127.0.0.1:8200'
+
+The unseal key and root token are displayed below in case you want to
+seal/unseal the Vault or re-authenticate.
+
+Unseal Key: UTZ7HoZCu8dtWa/eSMKcwq1klhC/qFoDxHXmhRn4qnE=
+Root Token: root
+
root
토큰은 구성관리 관리자의 권한으로 가정한다.
$ export VAULT_ADDR='http://127.0.0.1:8200'
+$ vault login
+Token (will be hidden): root
+
+Success! You are now authenticated. The token information displayed below
+is already stored in the token helper. You do NOT need to run "vault login"
+again. Future Vault requests will automatically use this token.
+
+Key Value
+--- -----
+token root
+token_accessor w5LvrjTvDDcfjPHrnOj6ib7E
+token_duration ∞
+token_renewable false
+token_policies ["root"]
+identity_policies []
+policies ["root"]
+
Spring Boot 앱에서 사용할 KV를 활성화 한다.
$ vault secrets enable -path=demo-app -version=2 kv
+
+Success! Enabled the kv secrets engine at: demo-app/
+
예제에서는 구성관리에서 MySQL 정보를 관리한다고 가정합니다. 관련 Spring Boot 앱은 spring initializr를 통해 생성한다.
테스트를 위한 종속성 목록은 다음과 같다.
Dependencies | 설명 |
---|---|
Spring Web | Spring MVC를 사용하여 RESTful을 포함한 웹 애플리케이션 구축에 사용 |
MySQL Driver | MySQL을 사용하기위한 드라이버 (MySQL 없는 경우 생략) |
Spring Data JPA | JPA를 사용하기 편하도록 만들어놓은 모듈 (MySQL 없는 경우 생략) |
Vault Configuration | 분산 시스템에서 외부화된 볼트 구성에 대한 클라이언트 측 지원을 제공 |
Lombok | 기계적인 코드들을 어노테이션을 기반으로 코드를 자동화하여 작성해주는 Java의 라이브러리 |
MySQL의 경우 다음과 같이 구성한다.
CREATE DATABASE java_dev_db;
+CREATE USER 'dev-user'@'%' IDENTIFIED BY 'dev-password';
+GRANT ALL PRIVILEGES ON java_dev_db.* TO 'dev-user'@'%';
+
앱에서 사용할 구성을 볼트의 demo-app/java_and_vault/dev
에 추가한다. 엔드포인트 정보의 조합은 <kv_endpoint>/<app_name>/<profile>
이다. 다음과 같이 CLI를 사용하여 구성 정보를 추가한다.
$ vault kv put demo-app/java_and_vault/dev \
+ app.config.auth.token=MY-AUTH-TOKEN-DEV-0000 \
+ app.config.auth.username=dev-user \
+ spring.datasource.database=java_dev_db \
+ spring.datasource.password=dev-password \
+ spring.datasource.username=dev-user
+
UI에서 확인해보면 결과는 다음과 같다.
앱과 볼트 연동 구성을 위해 다음을 추가한다. 기존 application.properties
대신 application.yml
로 변경하여 구성한다.
spring:
+ application:
+ name: java_and_vault
+ cloud.vault:
+ host: 127.0.0.1
+ port: 8200
+ scheme: http
+ config:
+ lifecycle:
+ enabled: false
+ authentication: TOKEN
+ token: root
+ kv:
+ enabled: true
+ backend: demo-app
+ profile-separator: '/'
+ generic:
+ enabled: false
+ config:
+ import: vault://
+ datasource:
+ url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.database}
+
spring.cloud.vault
에 볼트 관련 설정이 추가된다. host
: 볼트 서버의 호스트이름 또는 IP를 설정한다.port
: 볼트 서버의 포트를 설정한다.scheme
: 볼트 서버와의 통신에 사용할 프로토콜을 설정한다.config.lifecycle.enabled
의 경우 동적인 시크릿에 대한 생명주기 관리 동작 여부를 설정한다. 여기서는 정적인 구성을 사용하므로 false
로 설정한다.spring.cloud.vault.authentication
은 관리자 테스트를 위해 TOKEN
으로 입력한다.spring.cloud.vault.token
은 관리자용 인증인 root
를 입력한다.spring.cloud.vault.kv
는 활성화한 KV 의 선언을 위한 계층이다. enalbed
: 활성화 여부를 boolean 값으로 설정한다.backend
: KV가 활성화된 엔드포인트 경로 이름을 입력한다. 기본 값은 secret
이다.spring.cloud.vault.generic
은 v1 타입의 KV 선언을 위한 계층이다. enalbed
: 활성화 여부를 boolean 값으로 설정한다. 사용되지 않으므로 false
로 설정한다.spring.config.import
에 vault://
를 지정하여 볼트를 PropertySource로 마운트한다.spring.datasource
에서 MySQL 연동관련 정의를 설정한다. url
: DB Connection Url을 명시한다.database
: DB의 이름을 정의한다. 여기서는 볼트에서 해당 값을 가져온다.username
: DB 계정 사용자 이름을 정의한다. 여기서는 볼트에서 해당 값을 가져오므로 생략되었다.password
: DB 계정 사용자 패스워드를 정의한다. 여기서는 볼트에서 해당 값을 가져오므로 생략되었다.기본 패키지 경로(e.g. src/main/java/com/example/demo
)에 다음의 Java 파일을 추가한다.
| AppConfiguration.java
package com.example.demo;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import lombok.Getter;
+import lombok.Setter;
+@Getter
+@Setter
+@Configuration
+@ConfigurationProperties("app.config.auth")
+public class AppConfiguration {
+ private String username;
+ private String token;
+}
+
@ConfigurationProperties
에 정의한 app.config.auth
로 마운팅된 볼트의 내용을 주입한다.AppConfiguration
클래스는 어노테이션 정의에 따라 볼트로부터 내부에 정의되는 변수 username
과 token
값이 할당된다.| AppService.java
package com.example.demo;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import javax.annotation.PostConstruct;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class AppService {
+ private final AppConfiguration appConfiguration;
+ @PostConstruct
+ public void readConfigs() {
+ log.info("Reading configuration {} - {}", appConfiguration.getToken(), appConfiguration.getUsername());
+ }
+}
+
readConfigs()
메소드에 로그 출력에서 볼트로부터 할당된 변수 값을 확인한다.| DemoApplication.java
package com.example.demo;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import javax.annotation.PostConstruct;
+import org.springframework.beans.factory.annotation.Value;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@SpringBootApplication
+public class DemoApplication {
+
+ @Value("${spring.datasource.username}")
+ private String ds_name;
+
+ @Value("${spring.datasource.password}")
+ private String ds_pw;
+
+ public static void main(String[] args) {
+ SpringApplication.run(DemoApplication.class, args);
+ }
+
+ @PostConstruct
+ public void readDBconfigs() {
+ log.info("Reading datasource config {} - {}", ds_name, ds_pw);
+ }
+}
+
@Value
로 볼트에서 가져오는 구성정보가 application.yml
에 정의되어야 하는 구성 정보에 주입된 값을 받아온다.readDBconfigs()
메소드에 로그 출력에서 볼트로부터 할당된 구성 값을 확인한다.앱을 실행하여 구성을 가져오는지 확인한다.
$ gradle bootRun --args='--spring.profiles.active=dev'
+
+> Task :bootRun
+
+ . ____ _ __ _ _
+ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
+( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
+ \\/ ___)| |_)| | | | | || (_| | ) ) ) )
+ ' |____| .__|_| |_|_| |_\__, | / / / /
+ =========|_|==============|___/=/_/_/_/
+ :: Spring Boot :: (v3.0.5)
+
+# dev profile이 사용됨을 표기
+2023-04-06T17:15:58.395+09:00 INFO 48275 --- [ main]
+com.example.demo.DemoApplication : The following 1 profile is active: "dev"
+
+# 앱 구성의 spring.datasource 에서 정의하는 정보가 볼트에서 가져와서 실행되어 Connection Pool이 생성되고, 가져온 계정 정보가 출력됨을 확인
+2023-04-06T17:16:00.359+09:00 INFO 48275 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
+2023-04-06T17:16:00.614+09:00 INFO 48275 --- [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@57416e49
+2023-04-06T17:16:00.616+09:00 INFO 48275 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
+...
+2023-04-07T08:57:39.888+09:00 INFO 52598 --- [ main] com.example.demo.DemoApplication : Reading datasource config dev-user - dev-password
+
+# 앱 구성 app.config.auth 항목을 볼트에서 가져와서 출력됨을 확인
+2023-04-06T17:16:01.363+09:00 INFO 48275 --- [ main] com.example.demo.AppService : Reading configuration MY-AUTH-TOKEN-DEV-0000 - dev-user
+
Example 1
에서는 볼트의 루트 사용자를 사용하여 모든 구성 값을 확인할 수 있지만 앱과 이를 배포하는 사람, 파이프라인은 특정 구성에 대한 정보만 확인할 수 있어야 한다. 여기서는 prd
프로파일을 위한 구성과 정책 정의에 대해 확인한다.
MySQL의 경우 다음과 같이 구성한다.
CREATE DATABASE java_prd_db;
+CREATE USER 'prd-user'@'%' IDENTIFIED BY 'prd-password';
+GRANT ALL PRIVILEGES ON java_prd_db.* TO 'prd-user'@'%';
+
prd
를 위한 구성정보를 볼트에 추가한다.
$ vault kv put demo-app/java_and_vault/prd \
+ app.config.auth.token=MY-AUTH-TOKEN-prd-1111 \
+ app.config.auth.username=prd-user \
+ spring.datasource.database=java_prd_db \
+ spring.datasource.password=prd-password \
+ spring.datasource.username=prd-user
+
구성 관리자를 위한 Policy java-and-vault-prd-admin.hcl
파일 내용 및 적용은 다음과 같다.
$ cat java-and-vault-prd-admin.hcl
+
+path "demo-app/data/java_and_vault/prd" {
+ capabilities = ["create", "update", "read"]
+}
+
+$ vault policy write java-and-vault-prd-admin java-and-vault-prd-admin.hcl
+
+Success! Uploaded policy: java-and-vault-prd-admin
+
구성을 읽을수만 있는 Policy java-and-vault-prd-read.hcl
파일 내용 및 적용은 다음과 같다.
$ cat java-and-vault-prd-read.hcl
+
+path "demo-app/data/java_and_vault/prd" {
+ capabilities = ["read"]
+}
+
+$ vault policy write java-and-vault-prd-read java-and-vault-prd-read.hcl
+
+Success! Uploaded policy: java-and-vault-prd-read
+
앱을 위한 계정을 발급하기위한 Policy인 java-and-vault-prd-approle.hcl
파일 내용은 다음과 같다.
$ cat java-and-vault-prd-approle.hcl
+
+path "auth/approle/role/java-vault-prd/role-id" {
+ capabilities = ["read"]
+}
+
+path "auth/approle/role/java-vault-prd/secret-id" {
+ capabilities = ["create", "update"]
+}
+
+$ vault policy write java-and-vault-prd-approle java-and-vault-prd-approle.hcl
+
+Success! Uploaded policy: java-and-vault-prd-approle
+
관리자에게 java-and-vault-prd-admin
, java-and-vault-prd-approle
를 부여하여 구성에 대한 관리와 앱을위한 계정 발급 권한을 준다.
# 활성화 되어있지 않다면 userpass Auth Method 활성화
+$ vault auth enable userpass
+
+Success! Enabled userpass auth method at: userpass/
+
+$ vault write auth/userpass/users/app-prd-admin password=password policies=java-and-vault-prd-admin,java-and-vault-prd-approle
+
+Success! Data written to: auth/userpass/users/app-prd-admin
+
앱을 위한 AppRole인증에 java-and-vault-prd-read
를 추가한다.
# 활성화 되어있지 않다면 approle Auth Method 활성화
+$ vault auth enable approle
+
+Success! Enabled approle auth method at: approle/
+
+$ vault write auth/approle/role/java-vault-prd \
+ secret_id_ttl=10m \
+ token_period=24h \
+ policies="java-and-vault-prd-read"
+
+Success! Data written to: auth/approle/role/java-vault-prd
+
생성한 관리자 계정으로 로그인 하면 demo-app/java_and_vault/prd
의 구성 변경과 AppRole 계정의 secret-id
발급이 가능한지 확인한다. (별도의 터미널)
$ export VAULT_ADDR=http://127.0.0.1:8200
+$ vault login -method userpass username=app-prd-admin password=password
+
+Success! You are now authenticated. The token information displayed below
+is already stored in the token helper. You do NOT need to run "vault login"
+again. Future Vault requests will automatically use this token.
+
+Key Value
+--- -----
+token hvs.CAESIAE31Vrf91UbPhV5O0eh8KM0Tky_7MGk5ThyRu4tJbhUGh4KHGh2cy50ZDdZZ09BdDRnRmpqdkVRcUJYOWR5YUI
+token_accessor 9XuvRw1jKWt99iwlZ146652v
+token_duration 768h
+token_renewable true
+token_policies ["default" "java-and-vault-prd-admin" "java-and-vault-prd-approle"]
+identity_policies []
+policies ["default" "java-and-vault-prd-admin" "java-and-vault-prd-approle"]
+token_meta_username app-prd-admin
+
+$ vault kv put demo-app/java_and_vault/prd \
+ app.config.auth.token=MY-AUTH-TOKEN-prd-1111 \
+ app.config.auth.username=prd-user \
+ spring.datasource.database=java_prd_db \
+ spring.datasource.password=prd-password \
+ spring.datasource.username=prd-user
+
+========== Secret Path ==========
+demo-app/data/java_and_vault/prd
+
+======= Metadata =======
+Key Value
+--- -----
+created_time 2023-04-07T01:54:45.464698Z
+custom_metadata <nil>
+deletion_time n/a
+destroyed false
+version 2
+
+$ vault read auth/approle/role/java-vault-prd/role-id
+
+Key Value
+--- -----
+role_id 53b96749-1234-fec1-05b8-760c29991d89
+
+$ vault write -f auth/approle/role/java-vault-prd/secret-id
+
+Key Value
+--- -----
+secret_id 69b144ae-543a-81e3-9afa-8b290d8efd75
+secret_id_accessor d9338290-f1ff-ca09-fbaf-742071afeaa6
+secret_id_num_uses 0
+secret_id_ttl 10m
+
앱에서 사용할 AppRole 계정으로 로그인 하면 demo-app/java_and_vault/prd
의 구성 변경을 읽을수는 있고 업데이트는 안되는 여부를 확인한다. (별도의 터미널)
$ export VAULT_ADDR=http://127.0.0.1:8200
+$ vault write auth/approle/login \
+ role_id=53b96749-1234-fec1-05b8-760c29991d89 \
+ secret_id=aebbc4ac-79e4-c529-8751-c52f2f31a3d7
+
+Key Value
+--- -----
+token hvs.CAESIC7bpDI_cDGLCpKl6rZ
+token_accessor guDRqHNpnJtpmFXqkqsahc2e
+token_duration 24h
+token_renewable true
+token_policies ["default" "java-and-vault-prd-read"]
+identity_policies []
+policies ["default" "java-and-vault-prd-read"]
+token_meta_role_name java-vault-prd
+
+# 앱용 계정은 부여된 권한에 읽기 권한이 있으므로 정보 확인
+$ VAULT_TOKEN=hvs.CAESIC7bpDI_cDGLCpKl6rZ vault kv get demo-app/java_and_vault/prd
+
+========== Secret Path ==========
+demo-app/data/java_and_vault/prd
+
+======= Metadata =======
+Key Value
+--- -----
+created_time 2023-04-07T01:54:45.464698Z
+custom_metadata <nil>
+deletion_time n/a
+destroyed false
+version 2
+
+=============== Data ===============
+Key Value
+--- -----
+app.config.auth.token MY-AUTH-TOKEN-prd-1111
+app.config.auth.username prd-user
+spring.datasource.database java_prd_db
+spring.datasource.password prd-password
+spring.datasource.username prd-user
+
+# 앱용 계정은 부여된 권한에 쓰기 권한이 없으므로 관련 요청시 권한 거부
+$ VAULT_TOKEN=hvs.CAESIC7bpDI_cDGLCpKl6rZ vault kv put demo-app/java_and_vault/prd \
+ app.config.auth.token=MY-AUTH-TOKEN-prd-2222
+
+Error writing data to demo-app/data/java_and_vault/prd: Error making API request.
+
+URL: PUT http://127.0.0.1:8200/v1/demo-app/data/java_and_vault/prd
+Code: 403. Errors:
+
+* 1 error occurred:
+ * permission denied
+
앱과 정책이 적용된 볼트 연동 구성을 위해 application.yml
를 수정한다.
spring:
+ application:
+ name: java_and_vault
+ cloud.vault:
+ host: 127.0.0.1
+ port: 8200
+ scheme: http
+ config:
+ lifecycle:
+ enabled: false
+ # authentication: TOKEN
+ # token: root
+ authentication: APPROLE
+ app-role:
+ role-id: 53b96749-1234-fec1-05b8-760c29991d89
+ secret-id: aebbc4ac-79e4-c529-8751-c52f2f31a3d7
+ role: db-kv-reader
+ app-role-path: approle
+ kv:
+ enabled: true
+ backend: demo-app
+ profile-separator: '/'
+ generic:
+ enabled: false
+ config:
+ import: vault://
+ datasource:
+ url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.database}
+
spring.cloud.vault.authentication
은 앱용 인증으로 생성한 방식인 APPROLE
을 설정한다.spring.cloud.vault.authentication.app-role
은 APPROLE
인증에 대한 선언을 위한 계층이다. role-id
: 발급한 role-id
를 설정한다.secret-id
: 발급한 secret-id
를 설정한다. secret-id
는 제한시간이 10m
이였으므로, 배포시마다 교체해주어 계정을 보호한다.role
: role-id
가 포함된 Approle의 이름을 설정한다.app-role-path
: 활성화된 Approle의 엔드포인트 경로 이름을 입력한다.앱을 실행하여 구성을 가져오는지 확인한다. prd
프로파일을 지정한다.
$ gradle bootRun --args='--spring.profiles.active=prd'
+
+> Task :bootRun
+
+ . ____ _ __ _ _
+ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
+( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
+ \\/ ___)| |_)| | | | | || (_| | ) ) ) )
+ ' |____| .__|_| |_|_| |_\__, | / / / /
+ =========|_|==============|___/=/_/_/_/
+ :: Spring Boot :: (v3.0.5)
+
+# prd profile이 사용됨을 표기
+2023-04-07T14:05:03.395+09:00 INFO 67782 --- [ main] com.example.demo.DemoApplication : The following 1 profile is active: "prd"
+
+# 앱 구성의 spring.datasource 에서 정의하는 정보가 볼트에서 가져온 계정 정보가 출력됨을 확인
+2023-04-07T14:05:05.099+09:00 INFO 67782 --- [ main] com.example.demo.DemoApplication : Reading datasource config prd-user - prd-password
+
+# 앱 구성 app.config.auth 항목을 볼트에서 가져와서 출력됨을 확인
+2023-04-07T14:05:05.103+09:00 INFO 67782 --- [ main] com.example.demo.AppService : Reading configuration MY-AUTH-TOKEN-prd-1111 - prd-user
+
권한이 없는 dev
프로파일을 지정하는 경우 구성 값을 가져오지 못하므로 앱이 실행될 때 에러가 발생한다.
팁
Inject Secrets into Terraform Using the Vault Provider
Terraform Enterprise/Terraform Cloud를 사용할 때 Workspace의 변수(Variable)를 Vault를 사용하여 설정하는 것은 Terraform의 TFE 프로바이더와 Vault Provider를 사용하여 가능하다.
이번 예제는 Terraform Configuration Template에서 Vault를 사용하는 예제이다. Vault 인증 시 AppRole인증을 사용하였으나 기타 지원되는 인증 방법을 사용할 수 있다.
AWS Provider 설정 시 필요한 access_key와 secret_key를 환경 변수 설정이 아니라 코드 실행 시 Vault AWS 시크릿 엔진을 사용하도록 구성된 예제로, 코드는 다음과 같이 4개의 파일로 구성된다.
❯ tree
+.
+├── ec2.tf
+├── provider.tf
+├── terraform.tfvars
+└── variables.tf
+
+0 directories, 4 files
+
경고
위 예제를 사용하기 위해서는 Vault 상의 AWS 시크릿 엔진이 구성되어 있어야 하고, 인증을 위한 AppRole 구성 그리고 정책이 사전에 설정되어 있어야 한다.
사용할 프로바이더로 aws(자원 배포 대상)와 vault를 지정.
terraform {
+ required_providers {
+ aws = {
+ source = "hashicorp/aws"
+ version = "3.23.0"
+ }
+ vault = {
+ source = "hashicorp/vault"
+ version = "2.17.0"
+ }
+ }
+}
+
+provider "vault" {
+ # It is strongly recommended to configure this provider through the environment variables described above, so that each user can have
+ # separate credentials set in the environment.
+ #
+ # This will default to using $VAULT_ADDR
+ # But can be set explicitly
+ # address = "https://vault.example.net:8200"
+ address = var.vault_addr
+
+
+ auth_login {
+ path = "auth/approle/login"
+ parameters = {
+ role_id = var.login_approle_role_id
+ secret_id = var.login_approle_secret_id
+ }
+ }
+}
+
+# 코드 실행 시 Vault AWS 시크릿 엔진을 사용하여, data 값으로 access_key와 secret_key 생성하여 사용
+provider "aws" {
+ region = var.region
+ access_key = data.vault_aws_access_credentials.creds.access_key
+ secret_key = data.vault_aws_access_credentials.creds.secret_key
+ # STS Token을 사용하지 않는 경우 주석 처리
+ token = data.vault_aws_access_credentials.creds.security_token
+}
+
+
data 소스를 이용하여 Vault에 설정된 AWS 시크릿 엔진을 읽어서 access_key와 secret_key를 생성하고, 해당 정보를 provider에서 사용하게 된다.
data "vault_aws_access_credentials" "creds" {
+ # AWS 시크릿 엔진 경로 : 기본은 AWS
+ backend = var.aws_sec_path
+ # AWS 시크릿 엔진 구성 시 사용한 Role 이름
+ role = var.aws_sec_role
+ #STS Token으로 발급받아 설정. 아닌 경우, 다음 코드를 주석 처리 후 실행할 것.
+ type ="sts"
+}
+
+# AMI 정보 조회
+data "aws_ami" "ubuntu" {
+ most_recent = true
+
+ filter {
+ name = "name"
+ values = ["ubuntu/images/hvm-ssd/ubuntu-trusty-14.04-amd64-server-*"]
+ }
+
+ filter {
+ name = "virtualization-type"
+ values = ["hvm"]
+ }
+
+ owners = ["099720109477"] # Canonical
+}
+
+# Create AWS EC2 Instance
+resource "aws_instance" "main" {
+ ami = data.aws_ami.ubuntu.id
+ instance_type = "t2.nano"
+
+ tags = {
+ Name = var.name
+ TTL = var.ttl
+ owner = "${var.name}-guide"
+ }
+}
+
variable region {
+ default="ap-northeast-2"
+}
+
+variable "name" { default = "vault-dynamic-creds"}
+
+variable ttl { default = "24h"}
+
+variable "vault_addr" {
+ description = "Vault Server address format : http://IP_ADDRES:8200"
+ default = "http://127.0.0.1:8200"
+}
+
+variable login_approle_role_id {
+ description = "AppRole의 Role ID값 설정"
+}
+variable login_approle_secret_id {
+ description = "AppRole의 Secret ID값 설정"
+}
+#
+variable aws_sec_path {
+ description = "AWS 시크릿 엔진 경로, 마지막은 반드시 '/'로 끝나게 설정."
+ default = "aws/"
+}
+
+variable aws_sec_role {
+ description = "AWS 시크릿 엔진 상의 Role 이름"
+ default ="VAULT상에 생성된 AWS시크릿 엔진의 Role이름"
+}
+
vault_addr="http://127.0.0.1:8200"
+login_approle_role_id="AppRole의 Role_ID값"
+login_approle_secret_id="AppRole의 Secret_ID값"
+
+
wrk github : https://github.com/wg/wrk
transit : https://www.vaultproject.io/docs/secrets/transit
$ vault secrets enable transit
+Success! Enabled the transit secrets engine at: transit/
+
$ vault write -f transit/keys/my-key
+Success! Data written to: transit/keys/my-key
+
$ vault write transit/encrypt/my-key plaintext=$(base64 <<< "my secret data")
+
+Key Value
+--- -----
+ciphertext vault:v1:8SDd3WHDOjf7mq69CyCqYjBXAiQQAVZRkFM13ok481zoCmHnSeDX9vyf7w==
+
경고
X-Vault-Token
필요plaintext
데이터의 값은 base64 인코딩 필요curl \
+ -H "X-Vault-Token: s.HeeRXjkW1KJhF8ofQsglI9yw" \
+ -X POST \
+ -d "{\"plaintext\":\"dGhlIHF1aWNrIGJyb3duIGZveAo=\"}" \
+ http://192.168.60.103:8200/v1/transit/encrypt/my-key
+
팁
wrk 사용시 Vault Transit은 POST를 사용하므로 스크립트 작성이 필요
스크립트 작성
-- enc.lua
+wrk.method = "POST"
+wrk.body = "{\"plaintext\":\"dGhlIHF1aWNrIGJyb3duIGZveAo=\"}"
+wrk.headers["X-Vault-Token"] = "s.HeeRXjkW1KJhF8ofQsglI9yw"
+
실행
wrk -c 240 -t 8 -d 10s -s enc.lua http://192.168.60.103:8200/v1/transit/encrypt/my-key
+
스크립트 작성
-- dec.lua
+wrk.method = "POST"
+wrk.body = "{\"ciphertext\":\"vault:v1:I2JoSCrTduIDSI7BVIsFppwUop+YHFHejUbaHGeC7sb19CVZaYHEwicuJaXHxP/4\"}"
+wrk.headers["X-Vault-Token"] = "s.HeeRXjkW1KJhF8ofQsglI9yw"
+
실행
wrk -c 360 -t 12 -d 60s -s ./dec.lua http://192.168.60.103:8200/v1/transit/decrypt/my-key
+
개요
본 글에서는 HashiCorp Vault 및 Kubernetes 통합을 위해 HashiCorp가 지원하는 세 가지 방법을 자세히 비교한다:
각 방법에 대한 실용적인 지침(guidance)을 제공하여 사용 사례에 가장 적합한 방법을 이해하고 선택할 수 있도록 안내한다.
참고
본 포스트는 제품 설명서나 단계별(step-by-step) 구현 가이드가 아니며, HashiCorp Vault 및 Kubernetes에 익숙하고 시크릿 관리 개념에 대한 기본적인 이해가 있는 데브옵스 실무자를 위한 문서이다.
Vault 사이드카 에이전트 인젝터(Vault Sidecar Agent Injector)는 사이드카 패턴(sidecar pattern)을 활용하여 공유 메모리 볼륨에 볼트 시크릿을 렌더링하는 Vault 에이전트 컨테이너를 포함하도록 파드 사양(spec)을 변경한다. 공유 볼륨에 시크릿을 렌더링함으로써, 파드 내의 컨테이너는 Vault를 인식(Vault-aware)하지 않고도 Vault 시크릿을 사용할 수 있다.
인젝터는 Kubernetes Mutating Webhook Controller이다. Controller는 파드 이벤트를 가로채고(intercepts) 요청 내에 어노테이션이 있는 경우 파드에 변형(mutations)을 적용한다. 이 기능은 vault-k8s 프로젝트에 의해 제공되며, Vault Helm 차트를 사용하여 자동으로 설치 및 구성할 수 있다.
파드가 임시(ephemeral) CSI Secrets Store 볼륨을 사용하여 볼트 시크릿을 사용할 수 있도록 하는 것이 Vault CSI provider이다.
높은 수준(high level)에서, CSI Secretes Store 드라이버는 사용자가 SecretProviderClass
오브젝트를 생성할 수 있게 해준다. 이 오브젝트는 사용할 시크릿 프로바이더와 검색할 시크릿을 정의한다. CSI 볼륨을 요청하는 파드가 생성되면, CSI Secretes Store 드라이버는 프로바이더가 vault
인 경우 요청을 Vault CSI Provider에게 보낸다. 그러면 Vault CSI Provider는 지정된 SecretProviderClass
와 파드의 서비스 어카운트(SA)을 사용하여 볼트에서 시크릿을 검색하고 파드의 CSI 볼륨에 마운트한다. 시크릿은 Vault에서 검색되어 ContainerCreation
단계에서 CSI 시크릿 스토어 볼륨에 채워진다. 즉, Vault에서 시크릿을 읽고 볼륨에 쓰기 전까지는 파드가 시작되지 않도록 차단된다.
Vault Secrets Operator는 기본적으로 Vault secrets을 Kubernetes Secrets에 동기화할 책임이 있는(responsible) CRD 집합으로 쿠버네티스 시크릿 오퍼레이터(Kubernetes Secrets Operator)를 구현하는 새로운 통합 방법이다.
오퍼레이터는 하나 이상의 볼트 서버 인스턴스에서 정적(static), 동적(dynamic) 및 PKI 기반(PKI-based) 시크릿을 포함한 시크릿 관리의 전체 라이프사이클 동기화를 지원한다. 또한 오퍼레이터는 시크릿 로테이션(secret rotation)을 관리하고 Deployment의 rolling update를 통해 애플리케이션에 직접 알리거나(notifying) 롤링 업데이트를 트리거(triggering)하는 등 로테이션 후 작업을 수행할 수 있다.
참고 :
두 솔루션 간에는 몇 가지 유사점과 차이점이 있으며, Kubernetes 환경에서 시크릿 관리 전략을 설계하고 구현할 때 고려해야 할 사항이다.
고려사항 | 설명 |
---|---|
Secret projections | 모든 애플리케이션은 특정 방식으로 시크릿을 제공해야 한다. 일반적으로, 애플리케이션은 환경 변수로 내보내거나(exported) 애플리케이션 시작(startup) 시 애플리케이션이 읽을 수 있는 파일에 시크릿을 기록한다. 사용할 올바른 방법을 결정할 때 이 점을 염두에 두자. |
Secret scope | 일부 애플리케이션은 데이터센터, 엣지 또는 퍼블릭 클라우드의 여러 Kubernetes 환경(예: dev, qa, producton)에 배포된다. 일부 서비스는 가상 머신, 서버리스 또는 기타 클라우드 관리형(cloud-managed) 서비스에 배포된 외부 Kubernetes 환경에서 실행되기도 한다. 이러한 애플리케이션이 다양한 이기종 환경 전반에서 일련의 시크릿을 공유해야 하는 시나리오에 직면할 수 있다. 키의 범위(Scoping)를 올바르게 설정하여 Kubernetes 환경에 로컬 또는 여러 환경에 걸쳐 전역(global)으로 설정하면 각 애플리케이션이 배포된 환경 내에서 자체 키 집합에 쉽고 안전하게 액세스할 수 있다. |
Secret types | 시크릿은 텍스트 파일, 바이너리 파일, 토큰 또는 인증서 등이 대표적이다. 정적으로 생성하거나 동적으로 생성할 수도 있다. 영구적으로 유효하거나 시간 제한적(time-scoped)으로 유효할 수 있다. 또한 크기도 다양하다. 애플리케이션에 필요한 시크릿 유형과 애플리케이션에 투영(projected)되는 방식을 고려해야 한다. |
Secret definition | 또한 각 비밀이 정의, 생성, 업데이트 및 제거되는 방법과 해당 프로세스와 관련된 도구도 고려해야 한다. |
Encryption | 미사용(at rest) 시크릿과 전송(transit) 중인 시크릿을 모두 암호화하는 것은 많은 기업 조직에서 중요한 요구 사항이다. |
Governance | 애플리케이션과 비밀은 다대다(many-to-many) 관계를 가질 수 있으므로 애플리케이션이 각각의 비밀을 검색할 수 있도록 액세스 권한을 부여할 때 신중한 고려가 필요하다. 애플리케이션과 암호의 수(scale)가 증가함에 따라 액세스 정책 관리의 어려움도 커진다. |
Secrets updates and rotation | 시크릿은 임대(leased), 시간 범위(time-scoped) 지정 또는 자동으로 순환(rotated)될 수 있으며, 각 시나리오는 새 시크릿이 애플리케이션 파드에 올바르게 전파되도록 프로그래밍(programmatic) 프로세스를 거쳐야 한다. |
Secret caching | 특정 쿠버네티스 환경(예: edge 또는 retail)에서는 환경과 시크릿 스토리지 간의 통신 또는 네트워크 장애가 발생할 경우 시크릿 캐싱이 필요할 수 있다. |
Auditability | 모든 시크릿 액세스 정보를 자세히 설명하는 시크릿 액세스 감사 로그를 보관하는 것은 시크릿 액세스(secret-access) 이벤트의 추적성(traceability)을 보장(Keeping)하는 데 중요하다. |
이러한 설계 고려 사항을 염두에 두고, 두 통합 솔루션의 유사점(similarities)과 차이점(differences)을 살펴본다.
Vault Operator
, CSI
및 Sidecar
솔루션:
Vault에 저장된 다양한 유형의 시크릿 검색을 간소화(Simplify)하고 사소하지 않은(not-so-trivial) Vault 프로세스를 인식하지 않고도 Kubernetes에서 실행 중인 대상 파드에 시크릿을 노출한다. 중요한 점은 이러한 솔루션을 사용하기 위해 애플리케이션 로직이나 코드를 변경할 필요가 없기 때문에 브라운필드(brownfield) 애플리케이션을 Kubernetes로 더 쉽게 마이그레이션할 수 있다는 것이다. 그린필드(greenfield) 애플리케이션을 작업하는 개발자는 Vault SDKs를 활용하여 Vault와 직접 통합할 수 있다.
모든 유형의 Vault secrets engines을 지원한다. 즉, 정적 키-값(key-value) 시크릿부터 동적으로 생성된 데이터베이스 자격 증명(credentials), 사용자 정의 TTL이 포함된 TLS 인증서까지 광범위한(extensive) 시크릿 유형 세트를 활용할 수 있다.
애플리케이션의 Kubernetes 포드 서비스 어카운트(SA) 토큰을 “Secret Zero”로 활용하여 Kubernetes Auth Method를 통해 Vault에 인증한다.
즉, Vault에 인증할 때 애플리케이션 파드를 식별하기 위해 또 다른 별도의 ID를 관리할 필요가 없다.
세 가지 솔루션의 차이점은 다음과 같다:
hostPath
를 사용하여 임시 볼륨을 파드에 마운트하는데, 일부 컨테이너 플랫폼(예: OpenShift)은 기본적으로 이 기능을 비활성화한다. 반면, Sidecar Agent Service는 인메모리(in-memory) tmpfs 볼륨을 사용한다.아래 표는 두 솔루션을 개략적(high-level)으로 비교한 것이다:
Sidecar | CSI | Vault Operator | |
---|---|---|---|
Secret projection | 공유 메모리 볼륨 환경 변수* | 임시 디스크 환경변수 쿠버네티스 시크릿 | 쿠버네티스 시크릿 쿠버네티스 시크릿 볼륨 환경변수 |
Secret scope | 전역(Global) | 전역(Global) | 전역(Global) |
Secret types | 모든 볼트 시크릿 엔진 (정적 & 동적) | 모든 볼트 시크릿 엔진 (정적 & 동적) | 모든 볼트 시크릿 엔진 (정적 & 동적) |
Secret templating | ✅ | ❌ | ✅(+1.15) |
Secret size limit | Vault w/Consul Backend: 512 KB(기본) 제한없음 Vault w/Integrated Storage Backend: 1MiB(기본) 제한없음 | Vault w/Consul Backend: 512 KB(기본) 제한없음 Vault w/Integrated Storage Backend: 1MiB(기본) 제한없음 | Vault w/Consul Backend: 512 KB(기본) 제한없음 Vault w/Integrated Storage Backend: 1MiB(기본) 제한없음 |
Secret definitions | Vault CLI / API / UI | Vault CLI / API / UI | Vault CLI / API / UI |
Encryption | 지원(at rest & in-transit) | 지원(at rest & in-transit) | at-rest : etcd 저장소 암호화 시in-transit : TLS 사용 시 |
Secret rotation | ✅ | ❌ | ✅ |
Secret caching | ✅ | ❌ | ✅ |
Auditability | ✅ | ✅ | ✅ |
Deployment method | 1개의 공유된 K8s 클러스터 서비스 + 1개의 사이드카 컨테이너 | 데몬셋 | 디플로이먼트 |
Vault agent support | ✅ | ❌ | ❌ |
Helm support | ✅ | ✅ | ✅ |
Custom Support | ❌ | ❌ | ✅ |
K8s Secrets drift detection and automation remediation | ❌ | ❌ | ✅ |
***** achieved through Agent templating
겉으로 보기에 Kubernetes native secrets은 위에 제시된 두 가지 접근 방식과 비슷해 보일 수 있지만, 두 접근 방식에는 큰 차이점이 있다:
Kubernetes 시크릿 관리 솔루션이 아닙니다. 기본적으로 시크릿을 지원하지만, 이는 엔터프라이즈 시크릿 관리 솔루션과는 상당히 다르다. Kubernetes 시크릿은 클러스터로만 범위가 제한되며, 많은 애플리케이션은 일부 서비스를 Kubernetes 외부 또는 다른 Kubernetes 클러스터에서 실행한다. 따라서 설계 프로세스의 일부로 시크릿 범위를 고려하는 것이 중요하다. 이러한 애플리케이션이 Kubernetes 환경 외부에서 Kubernetes 시크릿을 사용하도록 하면 번거롭고 인증 및 권한 부여 문제가 발생할 수 있다.
Kubernetes 시크릿은 본질적으로(in nature) 정적(static)이다. 시크릿은 kubectl
또는 Kubernetes API
를 사용하여 정의할 수 있지만, 일단 정의되면 etcd
에 저장되고 파드를 생성하는 동안에만 파드에 제공된다. 이로 인해 시크릿이 오래되거나, 구식이거나(outdated), 만료되는 시나리오가 발생할 수 있으며, 시크릿을 업데이트하고 회전하기 위해 추가 워크플로우가 필요하고 새 버전의 시크릿을 사용하기 위해 애플리케이션을 다시 배포(re-deploying)해야 한다. 이로 인해 복잡성(complexity)이 가중되고 시간이 낭비될 수 있다. 따라서 디자인 프로세스의 일부로 시크릿의 최신성(freshness), 업데이트 및 순환에 대한 요구 사항을 고려해야 한다.
시크릿 액세스 관리의 보안 모델은 Kubernetes RBAC 모델과 연결되어 있다. 이 모델은 Kubernetes에 익숙하지 않은 사용자에게는 채택하기 어려울 수 있다. 플랫폼에 구애받지 않는(platform-agnostic) 보안 거버넌스 모델을 채택하면 애플리케이션이 실행되는 방식과 위치에 관계없이 애플리케이션에 대한 워크플로우를 채택할 수 있다.
Kubernetes에서 시크릿 관리를 위한 설계는 쉬운 일이 아니다. 각각 장단점이 있는 여러 가지 접근 방식이 있다. 이 게시물에 제시된 옵션을 살펴보고 내부를 이해하여 사용 사례에 가장 적합한 옵션을 결정하시길 적극 권장한다.
Kubernetes(K8s)환경에서 외부 Vault(External Vault Server)와 연계하는 경우 일반적으로 kubernetes
인증방식을 활용하여 Vault와 K8s 간 플랫폼 수준에서의 인증을 처리하나, K8s로의 Cluster API에 대한 inbound가 막혀있는 경우 이같은 방식은 사용할 수 없다. 따라서 helm
, vso
같은 방식의 사용이 불가능하므로 Vault Agent를 Sidecar로 함께 배포하는 경우 수동으로 구성해주어야 한다.
팁
구성 과정은 Vault Agent를 BM/VM 환경에 구성하는 방식과 유사하며, 관련 구성 파일과 인증을 위한 정보를 Kubernetes 리소스를 활용한다는 차이가 있다.
테스트를 위한 Secret Engine은 kv-v2
이며, /secret
경로에 할당하였다.
vault secrets enable -version=2 -path=secret kv
+
+vault kv put secret/my-k8s-secret foo=my-k8s-secret-data
+
+vault policy write my-secret - <<EOF
+path "secret/data/my-k8s-secret" {
+ capabilities = ["read"]
+}
+EOF
+
AppRole 인증방식을 활성화 한다.
vault auth enable approle
+
AppRole의 role
을 생성한다.
vault write auth/approle/role/k8s-role \
+ secret_id_ttl=10m \
+ token_ttl=60m \
+ token_max_ttl=120m \
+ policies=my-secret
+
role_id
를 확인한다.
vault read auth/approle/role/k8s-role/role-id
+
role_id
와 secret_id
를 K8s의 Secret에 저장한다.
# kubectl create secret generic vault-approle --from-literal=role_id=<role-id-1234> --from-literal=secret_id=<s.1234567890abcdef>
+kubectl create secret generic vault-approle \
+ --from-literal=role_id=$(vault read -field=role_id auth/approle/role/k8s-role/role-id) \
+ --from-literal=secret_id=$(vault write -force -field=secret_id auth/approle/role/k8s-role/secret-id)
+
AppRole로 인증하는 Vault Agent를 위한 구성 파일을 vault-agent-config.hcl
에 설정한다. ConfigMap에 저장한다.
cat <<EOF | kubectl create configmap vault-agent-config --from-file=agent-config.hcl=/dev/stdin
+vault {
+ address = "http://10.100.11.233:8200"
+}
+
+auto_auth {
+ method "approle" {
+ config = {
+ role_id_file_path = "/etc/vault/approle/role_id"
+ secret_id_file_path = "/etc/vault/approle/secret_id"
+ }
+ }
+
+ sink "file" {
+ config = {
+ path = "/etc/vault-agent-token/token"
+ }
+ }
+}
+
+template_config {
+ static_secret_render_interval = "20s"
+}
+
+template {
+ destination = "/etc/secrets/index.html"
+ contents = <<EOH
+ <html>
+ <body>
+ <p>Secret Value: {{ with secret "secret/data/my-k8s-secret" }}{{ .Data.data.foo }}{{ end }}</p>
+ </body>
+ </html>
+ EOH
+}
+EOF
+
AppRole ID과 SecretID, Vault Agent Config 를 사용하는 샘플 앱을 실행한다. 다음은 Nginx를 사용한 Deployment Yaml의 예이다.
kubectl apply -f - <<EOF
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: nginx-vault-demo
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: nginx-vault-demo
+ template:
+ metadata:
+ labels:
+ app: nginx-vault-demo
+ spec:
+ containers:
+ - name: nginx
+ image: nginx:latest
+ ports:
+ - containerPort: 80
+ volumeMounts:
+ - name: html-volume
+ mountPath: /usr/share/nginx/html
+ - name: vault-agent-sidecar
+ image: hashicorp/vault:latest
+ args:
+ - "agent"
+ - "-config=/etc/vault/agent-config.hcl"
+ volumeMounts:
+ - name: vault-agent-config
+ mountPath: /etc/vault
+ - name: vault-approle
+ mountPath: /etc/vault/approle
+ - name: vault-token
+ mountPath: /etc/vault-agent-token
+ - name: html-volume
+ mountPath: /etc/secrets
+ volumes:
+ - name: vault-agent-config
+ configMap:
+ name: vault-agent-config
+ - name: vault-approle
+ secret:
+ secretName: vault-approle
+ - name: vault-token
+ emptyDir: {}
+ - name: html-volume
+ emptyDir: {}
+EOF
+
Nginx의 Service를 등록한다.
kubectl apply -f - <<EOF
+apiVersion: v1
+kind: Service
+metadata:
+ name: nginx-service
+spec:
+ selector:
+ app: nginx-vault-demo
+ ports:
+ - protocol: TCP
+ port: 80
+ targetPort: 80
+EOF
+
port-forward
를 이용하여 Nginx에서 정상적으로 랜더링된 vault의 시크릿을 포함한 페이지가 나타나는지 확인한다.
kubectl port-forward $(kubectl get pods -l app=nginx-vault-demo -o jsonpath='{.items[0].metadata.name}') 8080:80
+
Vault Agent 구성파일에서 static_secret_render_interval
에 대한 정의가 있으므로, 20s 간격마다 변경된 KV 값으로 랜더링하는지 확인해본다.
vault kv put secret/my-k8s-secret foo=my-k8s-secret-data-v2
+
Pod 내의 vault-agent-sidecar
로그에 rendered
로그가 기록된다.
│ 2023-12-04T02:01:51.992Z [INFO] (runner) rendered "(dynamic)" => "/etc/secrets/index.html"
+
범용적인 AppRole 대신 Cloud Provider와의 인증 방식(여기서는 AWS 인증 방식)을 사용하여 Vault와 통신하는 구성을 적용할 수 있다.
EKS의 경우, EKS에 배포되는 Vault Agent는 AWS Role을 확인 가능하므로, AWS 인증 방식은 Vault가 AWS의 IAM 자격증명을 사용하여 인증을 수행하게 된다.
Vault AWS 인증 방식을 사용하기 위해서는 사전에 Vault AWS 인증에 사용할 Role이 필요하다.(아래는 Terraform으로의 구성 예제이다.)
provider "aws" {
+ region = "ap-northeast-2"
+}
+
+resource "aws_iam_role" "eks_vault_auth_role" {
+ name = "eks-vault-auth-role"
+
+ assume_role_policy = jsonencode({
+ Version = "2012-10-17",
+ Statement = [
+ {
+ Action = "sts:AssumeRole",
+ Effect = "Allow",
+ Principal = {
+ Service = "ec2.amazonaws.com"
+ }
+ }
+ ]
+ })
+}
+
+# Vault에 접근할 수 있는 역할에 대한 정책 (필요에 따라 수정)
+resource "aws_iam_role_policy" "vault_access" {
+ name = "VaultAccess"
+ role = aws_iam_role.eks_vault_auth_role.id
+
+ policy = jsonencode({
+ Version = "2012-10-17",
+ Statement = [
+ {
+ Action = [
+ "ec2:DescribeInstances",
+ "ec2:DescribeTags"
+ ],
+ Effect = "Allow",
+ Resource = "*"
+ }
+ ]
+ })
+}
+
+output "role_arn" {
+ value = aws_iam_role.eks_vault_auth_role.arn
+}
+
Vault 서버에서 AWS 인증 방식을 활성화한다.
vault auth enable aws
+
AWS 역할을 생성하고, 해당 역할에 적절한 정책을 할당한다. 이 역할은 EKS에서 실행되는 서비스나 애플리케이션이 Vault에 인증할 때 사용된다. (terraform으로 생성한 경우 role_arn output에 출력된 결과를 bound_iam_principal_arn
에 입력해준다.)
vault write auth/aws/role/k8s-role \
+ auth_type=iam \
+ bound_iam_principal_arn="arn:aws:iam::<AWS_ACCOUNT_ID>:role/<EKS_ROLE_NAME>" \
+ policies=my-secret \
+ ttl=1h
+
AWS로 인증하는 Vault Agent를 위한 구성 파일을 vault-agent-config-aws.hcl
에 설정한다. ConfigMap에 저장한다.
cat <<EOF | kubectl create configmap vault-agent-config-aws --from-file=agent-config.hcl=/dev/stdin
+vault {
+ address = "http://10.100.11.233:8200"
+}
+
+auto_auth {
+ method "aws" {
+ mount_path = "auth/aws"
+ config = {
+ type = "iam"
+ role = "k8s-role"
+ }
+ }
+
+ sink "file" {
+ config = {
+ path = "/etc/vault-agent-token/token"
+ }
+ }
+}
+
+template_config {
+ static_secret_render_interval = "20s"
+}
+
+template {
+ destination = "/etc/secrets/index.html"
+ contents = <<EOH
+ <html>
+ <body>
+ <p>Secret Value: {{ with secret "secret/data/my-k8s-secret" }}{{ .Data.data.foo }}{{ end }}</p>
+ </body>
+ </html>
+ EOH
+}
+EOF
+
Deployment는 다음과 같이 수정하여 적용한다. AppRole 구성에서의 관련 설정들이 제외된다.
kubectl apply -f - <<EOF
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: nginx-vault-demo
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: nginx-vault-demo
+ template:
+ metadata:
+ labels:
+ app: nginx-vault-demo
+ spec:
+ containers:
+ - name: nginx
+ image: nginx:latest
+ ports:
+ - containerPort: 80
+ volumeMounts:
+ - name: html-volume
+ mountPath: /usr/share/nginx/html
+ - name: vault-agent-sidecar
+ image: hashicorp/vault:latest
+ args:
+ - "agent"
+ - "-config=/etc/vault/agent-config.hcl"
+ volumeMounts:
+ - name: vault-agent-config-aws
+ mountPath: /etc/vault
+ - name: vault-token
+ mountPath: /etc/vault-agent-token
+ - name: html-volume
+ mountPath: /etc/secrets
+ volumes:
+ - name: vault-agent-config
+ configMap:
+ name: vault-agent-config
+ - name: vault-token
+ emptyDir: {}
+ - name: html-volume
+ emptyDir: {}
+EOF
+
Vault에 저장된 시크릿 또는 발행되는(Dynamic) 시크릿을 획득하기 위해서는, 시크릿을 요청하는 클라이언트(사람/앱/장비)가 다음의 과정을 수행해야 합니다.
Vault는 위의 과정을 클라이언트 대신 플랫폼 수준에서 대행할 수 있는 방안을 제공하고 있습니다. 여기서는 Kubernetes 상에서의 Vault와의 통합 구성을 활용하여 위 과정을 대체하고 Kubernetes 플랫폼 자체(Kuberetes Native)의 기능을 사용하듯 Vault의 시크릿을 사용하게 만드는 방식에 대해 설명합니다.
Kubernetes에 배포되는 컨테이너 애플리케이션이 Vault의 시크릿 데이터를 얻기위해 사용되는 플랫폼 수준(Kubernetes)에서의 통합을 설명합니다. CSI, Sidecar Injection, Vault Secret Operator VSO에 대한 설명은 다음 글을 확인해 보세요.
정보
아래 링크는 애플리케이션 또는 CICD 수준에서의 통합 예시 목록 입니다.
팁
Vault와 Kuberentes간의 통합의 세가지 방식은 중복으로 적용 가능합니다.
준비사항
구성을 위한 사전 필요 사항은 다음과 같습니다.
참고 : https://developer.hashicorp.com/vault/tutorials/kubernetes/kubernetes-secret-store-driver
CSI 방식에서는 SecretProviderClass
가 Vault의 정보를 구성하는 역할을 수행하고, 이후 deployment
에서 볼륨 형태로 호출하는 방식으로 구성됩니다.
Container Storage Interface(CSI) 드라이버를 설치하면 SecretProviderClass
CRD 구성을 사용하여 Kubernets에 외부 시크릿 저장소의 값을 Pod에 마운트 할 수 있습니다.
먼저 CSI 드라이버 Helm 차트를 등록합니다.
helm repo add secrets-store-csi-driver \
+ https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts
+
다음으로 CSI 드라이버를 설치 합니다.
helm install csi secrets-store-csi-driver/secrets-store-csi-driver \
+ --set syncSecret.enabled=true
+
설치가 정상적으로 완료되면 다음의 Pod를 확인할 수 있습니다.
$ kubectl get pods
+
+NAME READY STATUS RESTARTS AGE
+csi-secrets-store-csi-driver-vkppq 3/3 Running 0 20s
+
CSI 드라이버에서 vault
프로바이더를 사용하기 위한 구성을 설치해야 합니다. 이 구성이 설치되면 SecretProviderClass
정의 시 프로바이더 대상으로 vault
를 지정할 수 있습니다.
먼저 Vault Helm 차트를 등록합니다.
helm repo add hashicorp https://helm.releases.hashicorp.com
+
Vault Helm 차트를 사용하여 1) Kubernetes에 Vault를 설치하는 구성 또는 2) 외부 Vault와 연계하는 구성으로 설치 할 수 있습니다.
helm install vault hashicorp/vault \
+ --set "server.dev.enabled=true" \
+ --set "injector.enabled=false" \
+ --set "csi.enabled=true"
+
server.dev.enabled
: 개발 모드로 Vault 서버를 구성합니다. 운영 환경 구성시에는 사용하지 않습니다.injector.enabled
: Sidecar Injection 방식이 기본 활성화되므로 비활성으로 정의합니다.csi.enabled
: CSI 프로바이더 구성 설치를 위해 활성화 합니다.helm install vault hashicorp/vault \
+ --set "global.externalVaultAddr=$EXTERNAL_VAULT_ADDR" \
+ --set "injector.enabled=false" \
+ --set "csi.enabled=true"
+
global.externalVaultAddr
: 외부 Vault 주소를 입력 합니다.injector.enabled
: Sidecar Injection 방식이 기본 활성화되므로 비활성으로 정의합니다.csi.enabled
: CSI 프로바이더 구성 설치를 위해 활성화 합니다.설치가 정상적으로 완료되면 다음의 Pod를 확인할 수 있습니다. (vault-0
는 Vault 서버를 설치한 경우 확인되고, 외부 Vault 서버를 사용하는 경우에는 확인되지 않습니다.)
$ kubectl get pods
+
+NAME READY STATUS RESTARTS AGE
+vault-0 1/1 Running 0 58s
+vault-csi-provider-t874l 1/1 Running 0 58s
+
간단한 예로 Vault KV 시크릿 엔진을 사용합니다.
Kubernetes내의 Vault에서 CLI 사용
Kubernetes내에 배포된 Vault인 경우 다음과 같이 쉘을 실행할 수 있도록 Pod에 접근합니다. (Optional)
kubectl exec -it vault-0 -- /bin/sh
+
Vault가 개발 모드로 실행된 경우 기본적으로 Secret
이라는 경로에 KV version2 시크릿 엔진이 활성화되어있습니다. 만약 개발 모드가 아닌경우 다음과 같이 활성화 합니다. (Optional)
vault secrets enable -path secret -version=2 kv
+
secret/db-pass
경로에 password
값을 저장 합니다.
vault kv put secret/db-pass password="db-secret-password-v1"
+
다음과 같이 저장된 값을 확인할 수 있습니다.
$ vault kv get secret/db-pass
+
+=== Secret Path ===
+secret/data/db-pass
+
+======= Metadata =======
+Key Value
+--- -----
+created_time 2023-10-25T11:49:15.6993Z
+custom_metadata <nil>
+deletion_time n/a
+destroyed false
+version 1
+
+====== Data ======
+Key Value
+--- -----
+password db-secret-password-v1
+
Vault는 Kubernetes의 Service Account 토큰으로 인증할 수 있는 Kubernetes 인증 방식을 제공합니다. CSI 드라이버가 Vault에 저장된 시크릿 정보에 접근하여 시크릿을 획득하는 과정에서 Vault에 대한 인증/인가가 요구되므로 Kubernetes상의 리소스에서는 Kubernetes 인증 방식을 통해 Kubernetes의 방식으로 인증 받는 워크플로를 구성합니다.
Vault에 Kubernetes 인증 방식을 활성화 합니다.
vault auth enable kubernetes
+
Kubernetes API 주소를 Kubernetes 인증 방식 구성에 설정 합니다. 이 경우 자동으로 Vault Pod를 위한 자체 Service Account를 사용합니다.
vault write auth/kubernetes/config \
+ kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443"
+
vault write auth/kubernetes/config \
+ kubernetes_host="$EXTERNAL_VAULT_ADDR"
+
생성할 Kubernetes 인증 방식의 롤 정의에서 사용되는 정책을 구성합니다. Vault의 secret/data/db-pass
경로에 저장된 시크릿을 읽을 수 있는 정책 입니다.
vault policy write internal-app - <<EOF
+path "secret/data/db-pass" {
+ capabilities = ["read"]
+}
+EOF
+
$policy = @"
+path "secret/data/db-pass" {
+ capabilities = ["read"]
+}
+"@
+
+vault policy write injection-app - << $policy
+
예제의 롤 정의에서는 허용할 Service Account와 Kubernetes Namespace, 부여하는 정책으로 앞서 생성한 internal-app
정책을 할당합니다. 인증된 이후 유효 기간은 20분으로 설정 합니다.
vault write auth/kubernetes/role/database \
+ bound_service_account_names=webapp-sa \
+ bound_service_account_namespaces=default \
+ policies=internal-app \
+ ttl=20m
+
SecretProviderClass
를 사용하여 리소스 정의를 합니다. 정의를 할 뿐 시크릿을 읽는 동작을 수행하지는 않습니다. 다음 예제 리소스 spc-vault-database.yaml
파일에 설정한 정의는 vault
프로바이더를 사용하는 경우의 파라미터를 설명합니다.
apiVersion: secrets-store.csi.x-k8s.io/v1
+kind: SecretProviderClass
+metadata:
+ name: vault-database # CSI Provider로 호출될 이름
+spec:
+ provider: vault # CSI Provider 유형
+ parameters:
+ vaultAddress: "http://vault.default:8200"
+ # Vault에 구성한 Kubernetes 인증의 Role 이름
+ roleName: "database"
+ # Vault 주소 - 기본은 vault.default로 서비스 이름을 참조하나,
+ # 외부 Vault인경우 해당 주소를 지정해야 합니다.
+ vaultAddress: "https://vault.default:8200"
+ # Vault에 저장된 시크릿 경로와 대상을 지정합니다.
+ objects: |
+ - objectName: "db-password"
+ secretPath: "secret/data/db-pass"
+ secretKey: "password"
+
objects 항목은 리스트 구성으로 다수개의 시크릿을 정의할 수 있습니다.
objectName
: 해당 시크릿을 가리키는 이름으로, 최종적으로 이 이름으로 파일이 생성됨secretPath
: Vault에 정의된 시크릿 경로 (KV version2의 경우 API 구조적으로 활성화된 경로 뒤에 data
가 붙음)secretKey
: Vault의 시크릿 경로 호출시 반환되는 값의 키 이름설정한 spc-vault-database.yaml
를 적용합니다.
kubectl apply -f spc-vault-database.yaml
+
앞서 1) CSI에 사용될 Vault 프로바이더가 설치되고, 2) 인증이 구성되고, 3) 인증을 위한 롤이 정의되고, 4) Vault에 시크릿 값이 저장되고, 5) SecretProviderClass
가 정의되었습니다.
롤에서 정의한 허용하는 Service Account를 생성합니다.
kubectl create serviceaccount webapp-sa
+
앞서 생성된 SecretProviderClass
를 Volume
으로 정의하여 Pod 정의를 webapp-pod.yaml
에 저장합니다.
kind: Pod
+apiVersion: v1
+metadata:
+ name: webapp
+spec:
+ # 롤에서 허용하는 Service Account
+ serviceAccountName: webapp-sa
+ containers:
+ - image: jweissig/app:0.0.1
+ name: webapp
+ volumeMounts:
+ # 아래 volumes에서 정의한 csi 이름
+ - name: secrets-store-inline
+ # Pod에 마운트할 경로 지정
+ # 해당 경로 상에 SecretProviderClass에서 정의한 objectName으로 파일이 생성됨
+ mountPath: "/mnt/secrets-store"
+ # 마운트된 파일의 읽기/쓰기 여부
+ readOnly: true
+ volumes:
+ # volumeMounts에서 정의될 이름
+ - name: secrets-store-inline
+ csi:
+ driver: secrets-store.csi.k8s.io
+ # 마운트된 파일의 읽기/쓰기 여부
+ readOnly: true
+ volumeAttributes:
+ # SecretProviderClass로 정의한 이름
+ secretProviderClass: "vault-database"
+
webapp-pod.yaml
정의를 사용하여 Pod를 실행합니다. Pod가 실행되는 시점에 정의한 SecretProviderClass
에 의해 지정한 위치에 Vault에 저장된 시크릿이 마운트 됩니다.
kubectl apply -f webapp-pod.yaml
+
실행된 Pod를 확인합니다.
$ kubectl get pods
+
+NAME READY STATUS RESTARTS AGE
+webapp 1/1 Running 0 5m
+
Pod 내에 마운트된 시크릿 정보를 확인합니다.
$ kubectl exec webapp -- cat /mnt/secrets-store/db-password
+
+db-secret-password-v1
+
새로운 내용의 시크릿을 동일한 secret/db-pass
경로에 다시 저장합니다.
$ vault kv put secret/db-pass password="db-secret-password-v2"
+
+=== Secret Path ===
+secret/data/db-pass
+
+======= Metadata =======
+Key Value
+--- -----
+created_time 2023-10-27T00:06:52.910923Z
+custom_metadata <nil>
+deletion_time n/a
+destroyed false
+version 2
+
Vault의 시크릿이 변경되었지만 이전의 시크릿 정보로 마운트 된 기존 Pod에는 변경된 시크릿으로의 갱신이 발생하지 않습니다.
$ kubectl exec webapp -- cat /mnt/secrets-store/db-password
+
+db-secret-password-v1
+
이번 예제에서는 Pod를 실행하였으므로, 이미 실행된 Pod를 종료시키고 다시 실행해야 변경된 시크릿을 다시 CSI 드라이버로 요청하여 마운트 됩니다.
기존 pod를 삭제 합니다.
kubectl delete pod webapp
+
이전의 정의를 다시 사용하여 Pod를 실행합니다.
kubectl apply -f webapp-pod.yaml
+
Vault의 변경된 시크릿이 적용된 것을 확인 합니다.
$ kubectl exec webapp -- cat /mnt/secrets-store/db-password
+
+db-secret-password-v2
+
참고 1 : https://developer.hashicorp.com/vault/tutorials/kubernetes/kubernetes-sidecar
참고 2 : https://www.hashicorp.com/blog/injecting-vault-secrets-into-kubernetes-pods-via-a-sidecar
참고 3 : https://developer.hashicorp.com/vault/docs/platform/k8s/injector/annotations
참고 4 : https://devopscube.com/vault-agent-injector-tutorial/
BM/VM 환경에서는 Vault의 시크릿을 획득하고 갱신하는 과정을 지원하기 위해 Vault Agent를 활용할수 있습니다. Kubernetes에서는 애플리케이션 배포 시 Vault Agent를 사이트카로 구성하여 자동화된 구성과 해당 애플리케이션 만을 위한 Vault Agent를 제공할 수 있습니다.
사이드카 방식이 적용되도록 Kubernetes에 설치되면 Sidecar Injector
서비스가 실행되고, 이 서비스는 annotation
이 정의된 배포를 후킹하여 Vault Agent 컨테이너를 주입(Injection) 합니다.
Kubernetes에 Sidecar Injector
서비스를 구성을 설치해야 합니다. 이 구성이 설치되면 annotation
에 정의된 내용이 vault-k8s
webhook을 호출하여 Pod를 재정의하여 Vault Agent를 사이드카로 주입(Injection)합니다.
먼저 Vault Helm 차트를 등록합니다.
helm repo add hashicorp https://helm.releases.hashicorp.com
+
Vault Helm 차트를 사용하여 1) Kubernetes에 Vault를 설치하는 구성 또는 2) 외부 Vault와 연계하는 구성으로 설치 할 수 있습니다.
helm install vault hashicorp/vault \
+ --set "server.dev.enabled=true" \
+ --set "injector.enabled=true"
+
server.dev.enabled
: 개발 모드로 Vault 서버를 구성합니다. 운영 환경 구성시에는 사용하지 않습니다.injector.enabled
: Sidecar Injection 방식이 기본 값이 true
이나, 명시적으로 선언합니다.helm install vault hashicorp/vault \
+ --set "global.externalVaultAddr=$EXTERNAL_VAULT_ADDR" \
+ --set "injector.enabled=true"
+
global.externalVaultAddr
: 외부 Vault 주소를 입력 합니다.injector.enabled
: Sidecar Injection 방식이 기본 값이 true
이나, 명시적으로 선언합니다.설치가 정상적으로 완료되면 다음의 Pod를 확인할 수 있습니다.
vault-0
는 Vault 서버를 설치한 경우 확인되고, 외부 Vault 서버를 사용하는 경우에는 확인되지 않습니다.vault-agent-injector-*
Pod는 annotation
기반으로 사이드카를 주입하는 역할을 담당합니다.$ kubectl get pods
+
+NAME READY STATUS RESTARTS AGE
+vault-0 1/1 Running 0 80s
+vault-agent-injector-5945fb98b5-tpglz 1/1 Running 0 80s
+
Injection을 사용하여 Vault Agent를 사용할 수 있는 환경에서는 시크릿 업데이트를 자동으로 수행할 수 있고, KV 같은 정적(Static)인 시크릿의 경우 해당 시크릿의 생명주기 정보는 별도로 없기 때문에 이후 Injection 구성에서 변경을 확인할 시간 간격을 지정하게 됩니다.
예제에서는 KV를 활용합니다. Injection에서 사용할 KV 시크릿 엔진을 활성화합니다.
Kubernetes내의 Vault에서 CLI 사용
Kubernetes내에 배포된 Vault인 경우 다음과 같이 쉘을 실행할 수 있도록 Pod에 접근합니다. (Optional)
kubectl exec -it vault-0 -- /bin/sh
+
vault secrets enable -path for-injection -version=2 kv
+
for-injection/my-pass
경로에 password
값을 저장 합니다.
$ vault kv put for-injection/my-pass password="my-secret-password-v1"
+
+======= Secret Path =======
+for-injection/data/my-pass
+
+======= Metadata =======
+Key Value
+--- -----
+created_time 2023-10-27T00:41:16.656713Z
+custom_metadata <nil>
+deletion_time n/a
+destroyed false
+version 1
+
$ vault kv get for-injection/my-pass
+
+======= Secret Path =======
+for-injection/data/my-pass
+
+======= Metadata =======
+Key Value
+--- -----
+created_time 2023-10-27T00:41:16.656713Z
+custom_metadata <nil>
+deletion_time n/a
+destroyed false
+version 1
+
+====== Data ======
+Key Value
+--- -----
+password my-secret-password-v1
+
Injection을 사용하여 Vault Agent를 사용할 수 있는 환경에서는 시크릿 업데이트를 자동으로 수행할 수 있고, Database, PKI, Cloud Credential(AWS,Azure,GCP,Ali) 등 동적(Dynamic)인 시크릿의 경우 Vault Agent의 기존 방식 처럼 4/5 지점에서 갱신 작업을 수행 합니다.
예제에서는 PKI를 활용합니다. Injection에서 사용할 PKI 시크릿 엔진을 활성화 합니다.
Kubernetes내의 Vault에서 CLI 사용
Kubernetes내에 배포된 Vault인 경우 다음과 같이 쉘을 실행할 수 있도록 Pod에 접근합니다. (Optional)
kubectl exec -it vault-0 -- /bin/sh
+
vault secrets enable -path=pki pki
+vault secrets tune -max-lease-ttl=86400s -default-lease-ttl=3600s pki
+
루트 인증서를 생성합니다.
vault write -field=certificate pki/root/generate/internal \
+ common_name="test" \
+ ttl="86400h"
+
생성된 루트 인증서에 기반한 PKI 롤을 생성합니다. 예제에서는 동적 시크릿의 교체를 확인하기 위해 주기(ttl
, max_ttl
)를 짧게 구성합니다.
vault write pki/roles/my-role \
+ key_bits=4096 \
+ ttl="60s" \
+ max_ttl="60s" \
+ allow_ip_sans=true \
+ allowed_domains="example.com,my.domain" \
+ allow_subdomains=true
+
$ vault write pki/issue/my-role common_name=my-test.example.com
+
+Key Value
+--- -----
+ca_chain [-----BEGIN CERTIFICATE-----
+MIIDIDCCAgigAwIBAgIUR6Auk4MVpeis2oLq0StUwce/v/kwDQYJKoZIhvcNAQEL
+BQAwDzENMAsGA1UEAxMEdGVzdDAeFw0yMzEwMjYyMzUyNDlaFw0yMzEwMjcyMzUz
+MTlaMA8xDTALBgNVBAMTBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
+AoIBAQDXmlaX2Qu/rF+AFgWqJBCaNPziJrwsBB8nEUQh2S2XRMD9osoliWpaS33i
+iFAxc++Mec/FzKIsB7TskYWyFlv/GPmFG5gKdYfMuEMAgHrxM3OYWibQq0hDajJn
+oOcT1DwCx0mZqYdGoFVcw2TdW1vqgKRMx1vWBskaJHoGGpRvEPe7cYLz8itwqQfR
+7zkcVw3vdK6U50I7NnV/1wC+WOuwZ6IL5DKC1v3DtE5CrYKf/sBwDZfcdwFEjLpQ
+3hSXlVtv6t9E7QABcYqFkP5iebisNVP71L1Qk7oCuk4zqKpkbFytD6Nlf1LMRSFj
+SDt+aPuoqlmKrNtGsNcTqlW8k39HAgMBAAGjdDByMA4GA1UdDwEB/wQEAwIBBjAP
+BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTwEbHemyl86vBdxfMICjaKOJIoJzAf
+BgNVHSMEGDAWgBTwEbHemyl86vBdxfMICjaKOJIoJzAPBgNVHREECDAGggR0ZXN0
+MA0GCSqGSIb3DQEBCwUAA4IBAQB2y9QDCSNlr+j4v5H/7s4aZR8EWqbSdGc6F9w2
+FrR/bwo9eIxWiABFn/SH+bqHSK7fw4TMPJ0rEnJxBEvIPpA2kvGIxsBzPAdPzQ+A
+4F6tSJtiXB5A/7IZn9SQLUrcmcA5SuBGN9GjmPLpYSQg2ykJsTlExkYdg4co2sYV
+0F1gE5SXFGEmNTwFlpPSmKY6Zs8fKJZrzf+feCXFRlD/u+I4vftJqu7pwxZvPifR
+gPWi3kuzj71b4rEkZW3zNCP9XOtkCO/pNW2hJnc0QiTgQGvWXl/A8rIohsc+by2N
+MVr8w8iw1OdwbxI0LyC5siVgn+aER5qryYlpdeKR0/F2LuWX
+-----END CERTIFICATE-----]
+certificate -----BEGIN CERTIFICATE-----
+MIIETjCCAzagAwIBAgIUbrsk5aFaFZ5MB9aeS9DjXlcEvFswDQYJKoZIhvcNAQEL
+BQAwDzENMAsGA1UEAxMEdGVzdDAeFw0yMzEwMjcwMDU1MDlaFw0yMzEwMjcwMDU2
+MDJaMB4xHDAaBgNVBAMTE215LXRlc3QuZXhhbXBsZS5jb20wggIiMA0GCSqGSIb3
+DQEBAQUAA4ICDwAwggIKAoICAQDBSDy7gpekQv6Ro8p+4Szm8iavHv3KRyOoMYOv
+UdRlT+2KT6UcZGc9c0RLYS1yvT2QuYm6CeFLs+msYU/mVdLG/ih8YlCiOG9uDyZi
+CNqA+MOkxkwgChTfNgeOWQr8uo2J9CaV3bjProtE7weGaK/J5UYDTHxsZxMTom+t
+dMCAHol8d888cqVUvHXOth07/OKO5orKBcsxFhq0IAwERNT3kGxIcfOFvhWJfNUn
+ihdZMjq8u/CBaD3MhKU2Sn5e40FGLKuIoF0pMxhvPnJARiz53sAMMujoQxVgiIsQ
+9DT8phhNKXqufjOYEUUJ0hy/quy+/i4B00SPNsOOcD8vOsz96mhZC9ik4Avz0xdB
+KY0UaeULPmztdJW08dEaY1DSJB/k8rPMu4VZAFgxeFgj4byA9UwQ14aMJCZWHZYH
+cGbkJjcdFEC1ZhICKIHOO0KSoXpxD9xIQ1UWYvoegqSBSqvecaYf6y52kg7hb4rg
+jVFdBKWhBCGJ1RaqnbnBBp+Qk5AAkCyYfUpXXNmpYB7akIXLe3iTL50MkaiTd+GE
+xBXhfCYvwbpIZu35bAurwp3+nSTTeJw4d2O7s1L4iqdQ24fERYwEL8euLzzmxsjv
+qsmN1cHzbMulrCjVT3ZNBPFiMltoDJXyJDssKTM4nOpxr+FxBiCpbufcy2tDJ4eb
+svMxiQIDAQABo4GSMIGPMA4GA1UdDwEB/wQEAwIDqDAdBgNVHSUEFjAUBggrBgEF
+BQcDAQYIKwYBBQUHAwIwHQYDVR0OBBYEFGO3lOOstANAUseQaJmGMnCVQkw8MB8G
+A1UdIwQYMBaAFPARsd6bKXzq8F3F8wgKNoo4kignMB4GA1UdEQQXMBWCE215LXRl
+c3QuZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADggEBADT1aqab6RhLGuAvUgIS
+3lZ+B/ltWFQroFRgnfQArlMrVnCE1/7LAH+i7n8Ev7ixK0xP2CYRLwm8McLBEIjm
+qWB8ZXJJq4gXqZ6i5kIFvuRILkesSGJbs49TdeAMz6lyJd/BQmzM/uAhnqMrhlRt
+H6ZWnC5Z7dRGWT/yIlKL6kMcmxqEZCTt7j76V/8CRRUtxHtEgt4B4R/0lykWM8Ed
+HMok6crNYk94Jg/S8MWZlUHtCjDeXMd3mhDVQKaBNeLGjyugDF8KLVpcIMjEjglk
+UDG/bqxqwS2/jVUnDFvejbrOkJ/e3NefZa52/fZlXwqnwAlumtHOgEk3j00rHQSA
+/04=
+-----END CERTIFICATE-----
+expiration 1698368162
+issuing_ca -----BEGIN CERTIFICATE-----
+MIIDIDCCAgigAwIBAgIUR6Auk4MVpeis2oLq0StUwce/v/kwDQYJKoZIhvcNAQEL
+BQAwDzENMAsGA1UEAxMEdGVzdDAeFw0yMzEwMjYyMzUyNDlaFw0yMzEwMjcyMzUz
+MTlaMA8xDTALBgNVBAMTBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
+AoIBAQDXmlaX2Qu/rF+AFgWqJBCaNPziJrwsBB8nEUQh2S2XRMD9osoliWpaS33i
+iFAxc++Mec/FzKIsB7TskYWyFlv/GPmFG5gKdYfMuEMAgHrxM3OYWibQq0hDajJn
+oOcT1DwCx0mZqYdGoFVcw2TdW1vqgKRMx1vWBskaJHoGGpRvEPe7cYLz8itwqQfR
+7zkcVw3vdK6U50I7NnV/1wC+WOuwZ6IL5DKC1v3DtE5CrYKf/sBwDZfcdwFEjLpQ
+3hSXlVtv6t9E7QABcYqFkP5iebisNVP71L1Qk7oCuk4zqKpkbFytD6Nlf1LMRSFj
+SDt+aPuoqlmKrNtGsNcTqlW8k39HAgMBAAGjdDByMA4GA1UdDwEB/wQEAwIBBjAP
+BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTwEbHemyl86vBdxfMICjaKOJIoJzAf
+BgNVHSMEGDAWgBTwEbHemyl86vBdxfMICjaKOJIoJzAPBgNVHREECDAGggR0ZXN0
+MA0GCSqGSIb3DQEBCwUAA4IBAQB2y9QDCSNlr+j4v5H/7s4aZR8EWqbSdGc6F9w2
+FrR/bwo9eIxWiABFn/SH+bqHSK7fw4TMPJ0rEnJxBEvIPpA2kvGIxsBzPAdPzQ+A
+4F6tSJtiXB5A/7IZn9SQLUrcmcA5SuBGN9GjmPLpYSQg2ykJsTlExkYdg4co2sYV
+0F1gE5SXFGEmNTwFlpPSmKY6Zs8fKJZrzf+feCXFRlD/u+I4vftJqu7pwxZvPifR
+gPWi3kuzj71b4rEkZW3zNCP9XOtkCO/pNW2hJnc0QiTgQGvWXl/A8rIohsc+by2N
+MVr8w8iw1OdwbxI0LyC5siVgn+aER5qryYlpdeKR0/F2LuWX
+-----END CERTIFICATE-----
+private_key -----BEGIN RSA PRIVATE KEY-----
+MIIJKQIBAAKCAgEAwUg8u4KXpEL+kaPKfuEs5vImrx79ykcjqDGDr1HUZU/tik+l
+HGRnPXNES2Etcr09kLmJugnhS7PprGFP5lXSxv4ofGJQojhvbg8mYgjagPjDpMZM
+IAoU3zYHjlkK/LqNifQmld24z66LRO8HhmivyeVGA0x8bGcTE6JvrXTAgB6JfHfP
+PHKlVLx1zrYdO/zijuaKygXLMRYatCAMBETU95BsSHHzhb4ViXzVJ4oXWTI6vLvw
+gWg9zISlNkp+XuNBRiyriKBdKTMYbz5yQEYs+d7ADDLo6EMVYIiLEPQ0/KYYTSl6
+rn4zmBFFCdIcv6rsvv4uAdNEjzbDjnA/LzrM/epoWQvYpOAL89MXQSmNFGnlCz5s
+7XSVtPHRGmNQ0iQf5PKzzLuFWQBYMXhYI+G8gPVMENeGjCQmVh2WB3Bm5CY3HRRA
+tWYSAiiBzjtCkqF6cQ/cSENVFmL6HoKkgUqr3nGmH+sudpIO4W+K4I1RXQSloQQh
+idUWqp25wQafkJOQAJAsmH1KV1zZqWAe2pCFy3t4ky+dDJGok3fhhMQV4XwmL8G6
+SGbt+WwLq8Kd/p0k03icOHdju7NS+IqnUNuHxEWMBC/Hri885sbI76rJjdXB82zL
+pawo1U92TQTxYjJbaAyV8iQ7LCkzOJzqca/hcQYgqW7n3MtrQyeHm7LzMYkCAwEA
+AQKCAgBL3AhKKBVQWSMFEl4VslcnRX89WFKPo6AxEU3374wHP3mhwWSyYg3LJoR1
+eWyXDgMt3ERcCiisx649A+ySILkbdQF64DN5l+DUN4n/DC6GVBylfVa/dHWArfoF
+Opl/W9DVhkfmpiE1EfKDWbWAYXItMZlrDgf/m+z21dgzIhGzt0iK25MwzGZrfZRX
+T07mDnj1UTLD28ZGO8C7VaChxEo56Cs3u9GyekqFrcMTQ7WqQnafQLxCbiFjNeSK
+DG7Q2yzxV/LzKs2lr/I1JzM8Ws6oO27w2sJi9oFbY/wA6XgqeR4sms0V0154nr6T
+/i1eZL2KsMRp3vuXogzayN9jsBZoG3gXBE83nNK9/rXv7ExFXtlKTtzNPpJxQKYb
+YZ2LJf93vmmTYJLagTQxXHJKc2BXJJj3f09/0bXztr/gJDohTYZSuYA/c3H+ISl+
+AUZq4YI4hGOZi5e1iZYP1mUD9U42q107fXrb8HkVihaTptT2IPhYQtf+cRUpg84K
+yvAOp0VQm4xA+/NmKbV5buXYSsYh7ASTTc1LfwhsBNlsc5OUA8+EQ9GooJvS80wk
+xvsTeJ0Mml9KleY6Hw69JmZQEjbsQmLajJy4kvMQmT2NusJH/pbKrcDsazMjKqY8
+OMy+lsjUOp67mGvU7dxJC6ItkJfIpEWkJIjRUy/mF8gqSeI96QKCAQEA5YXZgsU/
+osFQZLY+qPe1tzUD/JYnwEmd4mTD/imNr+O0ZWAL8zqR8VtQsaeeQm9ktsUx3yyT
+GcXxwUiP1v4iFi9WryImD25rlGCbSNryUbf21XJec9DGptVYMxU5T1WDbfnVkX/7
+reWc8wnmhRDJ8/9rhjtlE9jUuJvs+rZt5n8Uz5t3cJqvsGTA1KcW4iU5l3nziAWj
+ZJebuZWrFgkL30cU4Py1Z1xS4tuNHeln0pF7IzKESSWFdoDB/8WBNQ1RqpscFGFn
+kPU/HirRbyT/S57v192lEHrKn0OoXketQqFqkc/xfRkVwD7bRske8/WCWcroUJl+
+dsuKGEVH3USD8wKCAQEA15Qlj2xVGJAjlf2oK6BcYLtIfBwzSc7PEojmq5u+ougS
+tyeV7NXsdbD+d95f3ZOW2b77jRe3nsWJKH5dgKoXZj5F1FbxE2KVDenAZ7OaQtml
+k1QtdNEI1v/qg4DtEmLBtYvbQK1fAsPe4PvajFYukI4SWO6/7LLKzFbKdl/0C4Qs
+QZVdFNfsBFJSYHCqkxpbhzY5t3hEK0uoVD9MEJSNPmgIHSxcnWRuMXSDVREBDEwS
+kmc+96KX4SEnn0pJ3NRQje6RhmWbb/bYNEpeFecNaAL0P9bUwYEIV3Je6bvOf9Nb
+71kouvbhRC17u36vrvMvdr9d7eg8kkch3QQVWhsfkwKCAQEAlouGsZmDNdOqUYSf
+8OAZFoP1i3VJuXwPzPDfBRRoVNf7+QpYjD78ftywPvZ8fYLnAmKxZXqtOZh2C5r2
+jcO+w+Jk7xZs9G4urfH3qH/DtQn/It2TSk/EHKWO5mKjZn/mZvoZtQfHIraajWcP
+BnSOojYEZtUKZUwxqqzLcV67ExaDpfCJFRjA5+gN+u1luwtDjTF2JN/d3hr7D201
+/IwOd3L+JNxcd+E8lIQBOX9gk+LMa7e0wO2VbrbhiEwZhZyo1khK0Kta7N+PeNAI
+8ufHc+hZ1LMSk46W3IPaKYzF/hA2AFHuSWlstN4FoZZFcSq1RwQqAMPNCUpT17uJ
+eX55NQKCAQEAuIX2IG156Sx3SUt1RuJcL/Aeex0oSWTbmeHUj88fvhEm897OVYpG
+e/aj2bZeGCrcVEVEy+AhK6WpYR/IqPjuTnW/D6Hbd9xJ+T67kggJYm8papIC1pqW
+FnG3KhiQ08v0QpETeqjrSlKd07W/u5+I+/Kfgb/aR6BCNeWUJv66xaC8wOY4Zj7r
+pkdQe3v0hTVqYrHndUNcFjMMQhBr60U8IM6rI011eMMeDvbL82Q6oWv7+ZSmMRDb
+L7hRUeckkgCpctNhfMg74/pF1XxSTC0ZLI5awsoAEiGAIlmjJC2882zWpGiMlHv9
+FX5ZCoPFnNpLJjlnDNxb/FkmgyebnyTYQQKCAQB/ef3oo/zb7OzESaIJrig52s2w
+4nyyvq6CcLZVMZ/8jN/3yU/SlzHpjdjTzS0ZFNCNPZyQKF5K9HXQAReTPBcobugc
+hlJc/EKAFxw2CZlH58qB2GMgUO0ZetHLiM+KU+AIhI/Hd+6iDUtFEduQSiWHzQth
+0F5bVH1MywUJIAXMvW4DOJetEqwHzYZ42PpJv8maWuqtaGsgv9wbDSdNy+ln5tya
+ubm4S+tIzeia5ucXFmy2xwWEOBATllxvNlBrDrwBCTgNDpJw1clo5Zz2tH1LGm/5
+G3bLC5clv3E3T/EXkst3LhcUIbRrsoQTIPeDQIyYqAurzECNCgfmyK5arNU4
+-----END RSA PRIVATE KEY-----
+private_key_type rsa
+serial_number 6e:bb:24:e5:a1:5a:15:9e:4c:07:d6:9e:4b:d0:e3:5e:57:04:bc:5b
+
Kubernetes내의 Vault에서 CLI 사용
Kubernetes내에 배포된 Vault인 경우 다음과 같이 쉘을 실행할 수 있도록 Pod에 접근합니다. (Optional)
kubectl exec -it vault-0 -- /bin/sh
+
Vault는 Kubernetes의 Service Account 토큰으로 인증할 수 있는 Kubernetes 인증 방식을 제공합니다. CSI 드라이버가 Vault에 저장된 시크릿 정보에 접근하여 시크릿을 획득하는 과정에서 Vault에 대한 인증/인가가 요구되므로 Kubernetes상의 리소스에서는 Kubernetes 인증 방식을 통해 Kubernetes의 방식으로 인증 받는 워크플로를 구성합니다.
Vault에 Kubernetes 인증 방식을 활성화 합니다. (이미 구성된 경우 실패합니다.)
vault auth enable kubernetes
+
Kubernetes API 주소를 Kubernetes 인증 방식 구성에 설정 합니다. 이 경우 자동으로 Vault Pod를 위한 자체 Service Account를 사용합니다.
vault write auth/kubernetes/config \
+ kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443"
+
vault write auth/kubernetes/config \
+ kubernetes_host="$EXTERNAL_VAULT_ADDR"
+
생성할 Kubernetes 인증 방식의 롤 정의에서 사용되는 정책을 구성합니다. 생서한 Vault KV, PKI의 경로에 저장된 시크릿을 읽고 발행할 수 있는 정책 입니다.
vault policy write injection-app - <<EOF
+path "for-injection/data/my-pass" {
+ capabilities = ["read"]
+}
+
+path "pki/issue/my-role" {
+ capabilities = ["create", "update"]
+}
+EOF
+
$policy = @"
+path "for-injection/data/my-pass" {
+ capabilities = ["read"]
+}
+
+path "pki/issue/my-role" {
+ capabilities = ["create", "update"]
+}
+"@
+
+vault policy write injection-app - << $policy
+
예제의 롤 정의에서는 허용할 Service Account와 Kubernetes Namespace, 부여하는 정책으로 앞서 생성한 injection-app
정책을 할당합니다. 인증된 이후 유효 기간은 20분으로 설정 합니다.
vault write auth/kubernetes/role/injection \
+ bound_service_account_names=webapp-vault \
+ bound_service_account_namespaces=default \
+ policies=injection-app \
+ ttl=20m
+
롤에서 정의한 허용하는 Service Account를 생성합니다.
kubectl create sa webapp-vault
+
다음과 같이 정의된 Deployment
를 vault-sidecar-deployment.yml
로 정의합니다.
apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: webapp-injection
+ labels:
+ app: issues
+spec:
+ selector:
+ matchLabels:
+ app: issues
+ replicas: 1
+ template:
+ metadata:
+ annotations:
+ vault.hashicorp.com/agent-inject: "true"
+ vault.hashicorp.com/agent-inject-status: "update"
+ # KV의 정적 시크릿 저장 대상 파일과 텔플릿 정의
+ vault.hashicorp.com/agent-inject-secret-my-config.txt: "for-injection/data/my-pass"
+ vault.hashicorp.com/agent-inject-template-my-config.txt: |
+ {{- with secret "for-injection/data/my-pass" -}}
+ password={{ .Data.data.password }}
+ {{- end }}
+ # PKI의 동적 시크릿 저장 대상 파일과 템플릿 정의 - Cert
+ vault.hashicorp.com/agent-inject-secret-cert.pem: "pki/issue/my-role"
+ vault.hashicorp.com/agent-inject-template-cert.pem: |
+ {{- with secret "pki/issue/my-role" "common_name=test.example.com" "ttl=30s" -}}
+ {{ .Data.certificate }}
+ {{ .Data.issuing_ca }}
+ {{- end }}
+ # PKI의 동적 시크릿 저장 대상 파일과 템플릿 정의 - Key
+ vault.hashicorp.com/agent-inject-secret-key.pem: "pki/issue/my-role"
+ vault.hashicorp.com/agent-inject-template-key.pem: |
+ {{- with secret "pki/issue/my-role" "common_name=test.example.com" "ttl=30s" -}}
+ {{ .Data.private_key }}
+ {{- end }}
+ # Vault의 Kubernetes인증으로 등록되어있는 Role 이름
+ vault.hashicorp.com/role: "injection"
+ vault.hashicorp.com/template-static-secret-render-interval: "10s"
+ labels:
+ app: issues
+ spec:
+ serviceAccountName: webapp-vault
+ containers:
+ - name: webapp
+ image: jweissig/app:0.0.1
+
spec.template.metadata.annotations
에 정의된 vault.hashicorp.com
의 설명은 다음과 같습니다.
vault.hashicorp.com/agent-inject
: Vault Agent의 사이드카 구성 여부를 지정vault.hashicorp.com/agent-inject-secret-\<filename>
: filename
영역에 문자 값으로 자동 랜더링하여 저장vault.hashicorp.com/agent-inject-template-\<filename>
: filename
영역의 문자값으로 파일을 생성할 때 사용자 정의 방식이 필요한 경우 사용vault.hashicorp.com/role
: kubernetes 인증에 정의한 롤 이름 지정vault.hashicorp.com/template-static-secret-render-interval
: 정적인 시크릿에 대한 검사 주기 설정정의한 vault-sidecar-deployment.yml
를 적용합니다.
kubectl apply -f vault-sidecar-deployment.yml
+
생성된 Pod를 확인 합니다.
$ kubectl get pods
+
+NAME READY STATUS RESTARTS AGE
+webapp-injection-7768d64fd9-89kqn 2/2 Running 0 9m18s
+
Pod 내에 애플리케이션 컨테이너 webapp
과 Vault Agent인 vault-agent
사이드카 컨테이너가 생성됨을 확인합니다.
$ kubectl get pods \
+ $(kubectl get pod -l app=issues -o jsonpath="{.items[0].metadata.name}") \
+ -o jsonpath='{.spec.containers[*].name}'
+
+webapp vault-agent
+
Pod 내 생성된 작용된 시크릿을 확인 합니다.
$ kubectl exec \
+ $(kubectl get pod -l app=issues -o jsonpath="{.items[0].metadata.name}") \
+ -c webapp -- cat /vault/secrets/my-config.txt
+
+password=my-secret-password-v1
+
$ kubectl exec \
+ $(kubectl get pod -l app=issues -o jsonpath="{.items[0].metadata.name}") \
+ -c webapp -- cat /vault/secrets/cert.pem
+
+-----BEGIN CERTIFICATE-----
+MIIESDCCAzCgAwIBAgIUKJqdIZIXXjE021NJzxnhP3Lihu0wDQYJKoZIhvcNAQEL
+...생략...
+Mk7y7BOjoZzXiqioAtk61FrjRwrc4vgJk9ESVeMnuJA8SorCAp3iNyVZJvU=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDIDCCAgigAwIBAgIUR6Auk4MVpeis2oLq0StUwce/v/kwDQYJKoZIhvcNAQEL
+...생략...
+MVr8w8iw1OdwbxI0LyC5siVgn+aER5qryYlpdeKR0/F2LuWX
+-----END CERTIFICATE-----
+
$ kubectl exec \
+ $(kubectl get pod -l app=issues -o jsonpath="{.items[0].metadata.name}") \
+ -c webapp -- cat /vault/secrets/key.pem
+
+-----BEGIN RSA PRIVATE KEY-----
+MIIJKAIBAAKCAgEAwSWRRLQ17lpKGaWGCOVaFIafd4NCe8u3LLMKSflutwMh3e7l
+...생략...
+cpxBcJwE+orZbH2TJAjVvqyfRrYflOLA6gqkbZd5W6zQClN3orb/E8UzEmg=
+-----END RSA PRIVATE KEY-----
+
팁
기본 마운트 경로가 /vault/secrets/<file-name>
이므로, 변경이 필요한경우 다음의 annotation
정의를 추가할 수 있습니다.
## 생략 ##
+spec:
+ template:
+ metadata:
+ annotations:
+ # 생략
+ vault.hashicorp.com/agent-inject-file-database-config: "/some/secret/here.txt"
+ vault.hashicorp.com/secret-volume-path-database-config: "/app"
+
결과 : /app/some/secret/here.txt
Vault Agent가 시크릿 데이터가 변경됨을 감지하고 대상 Pod에 저장되는 시크릿을 자동으로 갱신합니다.
정적 시크릿인 KV의 값을 변경해봅니다.
vault kv put for-injection/my-pass password="my-secret-password-v2"
+
다시 Pod내의 저장된 KV 시크릿 값을 확인합니다. 확인 간격을 10초로 정의했으므로, 갱신이 안된경우 다시 확인해봅니다.
$ kubectl exec \
+ $(kubectl get pod -l app=issues -o jsonpath="{.items[0].metadata.name}") \
+ -c webapp -- cat /vault/secrets/my-config.txt
+
+password=my-secret-password-v2
+
Vault가 생성하는 동적 시크릿은 lease
에서 생성 후 수명주기를 관리합니다. Vault Agent는 생성한 동적 시크릿의 수명주기를 확인하여 만료되기 전 자동 연장(Renewal) 및 자동 갱신 합니다.
예제의 인증서의 수명을 60s
로 정의하였으므로, 4/5 지점인 약 48초가 지나면 자동갱신됩니다. 앞서 랜더링된 인증서 확인 커맨드를 사용하여 변경되는 인증서를 확인합니다.
$ kubectl exec \
+ $(kubectl get pod -l app=issues -o jsonpath="{.items[0].metadata.name}") \
+ -c webapp -- cat /vault/secrets/cert.pem
+
+-----BEGIN CERTIFICATE-----
+...생략...
+Mk7y7BOjoZzXiqioAtk61FrjRwrc4vgJk9ESVeMnuJA8SorCAp3iNyVZJvU=
+-----END CERTIFICATE-----
+...생략...
+
$ kubectl exec \
+ $(kubectl get pod -l app=issues -o jsonpath="{.items[0].metadata.name}") \
+ -c webapp -- cat /vault/secrets/cert.pem
+
+-----BEGIN CERTIFICATE-----
+...생략...
+uDtXjqrZYtEI47dZjsVLxnBDLBoTRmzyxtywRezmvL2aMA5r9Z6WhhmFY2o=
+-----END CERTIFICATE-----
+...생략...
+
참고 1 : https://developer.hashicorp.com/vault/tutorials/kubernetes/vault-secrets-operator
참고 2 : https://developer.hashicorp.com/vault/docs/platform/k8s/vso
VSO를 사용하면 Pod가 기존 Kubernetes Secrets을 활용하여 시크릿을 사용하던 방식으로 Vault의 시크릿을 사용하게 됩니다. VSO는 CRD(Custom Resource Definitions)를 구성하여 Vault의 시크릿을 Kubernetes Secrets으로 동기화하는 동작을 수행합니다.
애플리케이션 수준, 또는 CSI와 사이드카 인젝션 방식의 구성 또한 Vault 시크릿을 사용하기 위한 다양한 방안을 제공했지만 애플리케이션 또는 기존 배포의 정의에 새로운 구성이 필요합니다. VSO는 Kubernetes의 Secret을 활용하여 기존 사용 경험을 유지하지만 Vault의 시크릿을 사용할 수 있는 방안을 제공합니다.
현재 지원되는 버전은 다음과 같습니다. 변경 사항은 링크를 참고하세요. (지원되는 Kubernetes 버전)
1.281.271.261.251.24Kubernetes에 Sidecar Injector
서비스를 구성을 설치해야 합니다. 이 구성이 설치되면 annotation
에 정의된 내용이 vault-k8s
webhook을 호출하여 Pod를 재정의하여 Vault Agent를 사이드카로 주입(Injection)합니다.
먼저 Vault Helm 차트를 등록합니다.
helm repo add hashicorp https://helm.releases.hashicorp.com
+
Vault Helm 차트를 사용하여 1) Kubernetes에 Vault를 설치하는 구성 또는 2) 외부 Vault와 연계하는 구성으로 설치 할 수 있습니다.
Vault가 별도 구성되어있지 않은 경우 해당 Kubernetes에 Vault 서버를 구성합니다.
helm install vault hashicorp/vault \
+ --set "server.dev.enabled=true" \
+ --set "injector.enabled=false" \
+ --set "csi.enabled=false"
+
아래와 같이 helm 설치 시 값을 정의한 vault-operator-values.yaml
파일을 생성합니다.
defaultVaultConnection:
+ enabled: true
+ address: http://vault.default.svc.cluster.local:8200
+ skipTLSVerify: false
+ spec:
+ template:
+ spec:
+ containers:
+ - name: manager
+ args:
+ - "--client-cache-persistence-model=direct-encrypted"
+
defaultVaultConnection:
+ enabled: true
+ # 연결 가능한 외부 Vault URL을 정의합니다.
+ address: "외부 Vault Url"
+ skipTLSVerify: false
+ spec:
+ template:
+ spec:
+ containers:
+ - name: manager
+ args:
+ - "--client-cache-persistence-model=direct-encrypted"
+
다음을 실행하여 정의한 helm의 값으로 설치를 진행합니다.
helm install vault-secrets-operator \ hashicorp/vault-secrets-operator \
+ -n vault-secrets-operator-system \
+ --create-namespace \
+ --values vault-operator-values.yaml
+
설치가 완료되면 다음의 Pod를 확인할 수 있습니다.
$ kubectl get pods -n vault-secrets-operator-system
+
+NAME READY STATUS RESTARTS AGE
+vault-secrets-operator-controller-manager-67879cb4d4-wzs6c 2/2 Running 0 4h22m
+
VSO에서 사용할 KV 시크릿 엔진을 활성화합니다.
Kubernetes내의 Vault에서 CLI 사용
Kubernetes내에 배포된 Vault인 경우 다음과 같이 쉘을 실행할 수 있도록 Pod에 접근합니다. (Optional)
kubectl exec -it vault-0 -- /bin/sh
+
vault secrets enable -path for-vso -version=2 kv
+
for-vso/my-pass
경로에 password
값을 저장 합니다.
$ vault kv put for-vso/my-pass password="my-secret-password-v1"
+
+==== Secret Path ====
+for-vso/data/my-pass
+
+======= Metadata =======
+Key Value
+--- -----
+created_time 2023-10-27T04:24:18.430362Z
+custom_metadata <nil>
+deletion_time n/a
+destroyed false
+version 1
+
$ vault kv get for-vso/my-pass
+
+==== Secret Path ====
+for-vso/data/my-pass
+
+======= Metadata =======
+Key Value
+--- -----
+created_time 2023-10-27T04:24:18.430362Z
+custom_metadata <nil>
+deletion_time n/a
+destroyed false
+version 1
+
+====== Data ======
+Key Value
+--- -----
+password my-secret-password-v1
+
PKI 시크릿의 경우 동적 시크릿으로, 발급 후 만료되기 전 Kubernetes Secret의 내용을 갱신 합니다.
Kubernetes내의 Vault에서 CLI 사용
Kubernetes내에 배포된 Vault인 경우 다음과 같이 쉘을 실행할 수 있도록 Pod에 접근합니다. (Optional)
kubectl exec -it vault-0 -- /bin/sh
+
vault secrets enable -path=pki pki
+vault secrets tune -max-lease-ttl=86400s -default-lease-ttl=3600s pki
+
루트 인증서를 생성합니다.
vault write -field=certificate pki/root/generate/internal \
+ common_name="test" \
+ ttl="86400h"
+
생성된 루트 인증서에 기반한 PKI 롤을 생성합니다. 예제에서는 동적 시크릿의 교체를 확인하기 위해 주기(ttl
, max_ttl
)를 짧게 구성합니다.
vault write pki/roles/my-role \
+ key_bits=4096 \
+ ttl="60s" \
+ max_ttl="60s" \
+ allow_ip_sans=true \
+ allowed_domains="example.com,my.domain" \
+ allow_subdomains=true
+
$ vault write pki/issue/my-role common_name=my-test.example.com
+
+Key Value
+--- -----
+ca_chain [-----BEGIN CERTIFICATE-----
+MIIDIDCCAgigAwIBAgIUR6Auk4MVpeis2oLq0StUwce/v/kwDQYJKoZIhvcNAQEL
+BQAwDzENMAsGA1UEAxMEdGVzdDAeFw0yMzEwMjYyMzUyNDlaFw0yMzEwMjcyMzUz
+MTlaMA8xDTALBgNVBAMTBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
+AoIBAQDXmlaX2Qu/rF+AFgWqJBCaNPziJrwsBB8nEUQh2S2XRMD9osoliWpaS33i
+iFAxc++Mec/FzKIsB7TskYWyFlv/GPmFG5gKdYfMuEMAgHrxM3OYWibQq0hDajJn
+oOcT1DwCx0mZqYdGoFVcw2TdW1vqgKRMx1vWBskaJHoGGpRvEPe7cYLz8itwqQfR
+7zkcVw3vdK6U50I7NnV/1wC+WOuwZ6IL5DKC1v3DtE5CrYKf/sBwDZfcdwFEjLpQ
+3hSXlVtv6t9E7QABcYqFkP5iebisNVP71L1Qk7oCuk4zqKpkbFytD6Nlf1LMRSFj
+SDt+aPuoqlmKrNtGsNcTqlW8k39HAgMBAAGjdDByMA4GA1UdDwEB/wQEAwIBBjAP
+BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTwEbHemyl86vBdxfMICjaKOJIoJzAf
+BgNVHSMEGDAWgBTwEbHemyl86vBdxfMICjaKOJIoJzAPBgNVHREECDAGggR0ZXN0
+MA0GCSqGSIb3DQEBCwUAA4IBAQB2y9QDCSNlr+j4v5H/7s4aZR8EWqbSdGc6F9w2
+FrR/bwo9eIxWiABFn/SH+bqHSK7fw4TMPJ0rEnJxBEvIPpA2kvGIxsBzPAdPzQ+A
+4F6tSJtiXB5A/7IZn9SQLUrcmcA5SuBGN9GjmPLpYSQg2ykJsTlExkYdg4co2sYV
+0F1gE5SXFGEmNTwFlpPSmKY6Zs8fKJZrzf+feCXFRlD/u+I4vftJqu7pwxZvPifR
+gPWi3kuzj71b4rEkZW3zNCP9XOtkCO/pNW2hJnc0QiTgQGvWXl/A8rIohsc+by2N
+MVr8w8iw1OdwbxI0LyC5siVgn+aER5qryYlpdeKR0/F2LuWX
+-----END CERTIFICATE-----]
+certificate -----BEGIN CERTIFICATE-----
+MIIETjCCAzagAwIBAgIUbrsk5aFaFZ5MB9aeS9DjXlcEvFswDQYJKoZIhvcNAQEL
+BQAwDzENMAsGA1UEAxMEdGVzdDAeFw0yMzEwMjcwMDU1MDlaFw0yMzEwMjcwMDU2
+MDJaMB4xHDAaBgNVBAMTE215LXRlc3QuZXhhbXBsZS5jb20wggIiMA0GCSqGSIb3
+DQEBAQUAA4ICDwAwggIKAoICAQDBSDy7gpekQv6Ro8p+4Szm8iavHv3KRyOoMYOv
+UdRlT+2KT6UcZGc9c0RLYS1yvT2QuYm6CeFLs+msYU/mVdLG/ih8YlCiOG9uDyZi
+CNqA+MOkxkwgChTfNgeOWQr8uo2J9CaV3bjProtE7weGaK/J5UYDTHxsZxMTom+t
+dMCAHol8d888cqVUvHXOth07/OKO5orKBcsxFhq0IAwERNT3kGxIcfOFvhWJfNUn
+ihdZMjq8u/CBaD3MhKU2Sn5e40FGLKuIoF0pMxhvPnJARiz53sAMMujoQxVgiIsQ
+9DT8phhNKXqufjOYEUUJ0hy/quy+/i4B00SPNsOOcD8vOsz96mhZC9ik4Avz0xdB
+KY0UaeULPmztdJW08dEaY1DSJB/k8rPMu4VZAFgxeFgj4byA9UwQ14aMJCZWHZYH
+cGbkJjcdFEC1ZhICKIHOO0KSoXpxD9xIQ1UWYvoegqSBSqvecaYf6y52kg7hb4rg
+jVFdBKWhBCGJ1RaqnbnBBp+Qk5AAkCyYfUpXXNmpYB7akIXLe3iTL50MkaiTd+GE
+xBXhfCYvwbpIZu35bAurwp3+nSTTeJw4d2O7s1L4iqdQ24fERYwEL8euLzzmxsjv
+qsmN1cHzbMulrCjVT3ZNBPFiMltoDJXyJDssKTM4nOpxr+FxBiCpbufcy2tDJ4eb
+svMxiQIDAQABo4GSMIGPMA4GA1UdDwEB/wQEAwIDqDAdBgNVHSUEFjAUBggrBgEF
+BQcDAQYIKwYBBQUHAwIwHQYDVR0OBBYEFGO3lOOstANAUseQaJmGMnCVQkw8MB8G
+A1UdIwQYMBaAFPARsd6bKXzq8F3F8wgKNoo4kignMB4GA1UdEQQXMBWCE215LXRl
+c3QuZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADggEBADT1aqab6RhLGuAvUgIS
+3lZ+B/ltWFQroFRgnfQArlMrVnCE1/7LAH+i7n8Ev7ixK0xP2CYRLwm8McLBEIjm
+qWB8ZXJJq4gXqZ6i5kIFvuRILkesSGJbs49TdeAMz6lyJd/BQmzM/uAhnqMrhlRt
+H6ZWnC5Z7dRGWT/yIlKL6kMcmxqEZCTt7j76V/8CRRUtxHtEgt4B4R/0lykWM8Ed
+HMok6crNYk94Jg/S8MWZlUHtCjDeXMd3mhDVQKaBNeLGjyugDF8KLVpcIMjEjglk
+UDG/bqxqwS2/jVUnDFvejbrOkJ/e3NefZa52/fZlXwqnwAlumtHOgEk3j00rHQSA
+/04=
+-----END CERTIFICATE-----
+expiration 1698368162
+issuing_ca -----BEGIN CERTIFICATE-----
+MIIDIDCCAgigAwIBAgIUR6Auk4MVpeis2oLq0StUwce/v/kwDQYJKoZIhvcNAQEL
+BQAwDzENMAsGA1UEAxMEdGVzdDAeFw0yMzEwMjYyMzUyNDlaFw0yMzEwMjcyMzUz
+MTlaMA8xDTALBgNVBAMTBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
+AoIBAQDXmlaX2Qu/rF+AFgWqJBCaNPziJrwsBB8nEUQh2S2XRMD9osoliWpaS33i
+iFAxc++Mec/FzKIsB7TskYWyFlv/GPmFG5gKdYfMuEMAgHrxM3OYWibQq0hDajJn
+oOcT1DwCx0mZqYdGoFVcw2TdW1vqgKRMx1vWBskaJHoGGpRvEPe7cYLz8itwqQfR
+7zkcVw3vdK6U50I7NnV/1wC+WOuwZ6IL5DKC1v3DtE5CrYKf/sBwDZfcdwFEjLpQ
+3hSXlVtv6t9E7QABcYqFkP5iebisNVP71L1Qk7oCuk4zqKpkbFytD6Nlf1LMRSFj
+SDt+aPuoqlmKrNtGsNcTqlW8k39HAgMBAAGjdDByMA4GA1UdDwEB/wQEAwIBBjAP
+BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTwEbHemyl86vBdxfMICjaKOJIoJzAf
+BgNVHSMEGDAWgBTwEbHemyl86vBdxfMICjaKOJIoJzAPBgNVHREECDAGggR0ZXN0
+MA0GCSqGSIb3DQEBCwUAA4IBAQB2y9QDCSNlr+j4v5H/7s4aZR8EWqbSdGc6F9w2
+FrR/bwo9eIxWiABFn/SH+bqHSK7fw4TMPJ0rEnJxBEvIPpA2kvGIxsBzPAdPzQ+A
+4F6tSJtiXB5A/7IZn9SQLUrcmcA5SuBGN9GjmPLpYSQg2ykJsTlExkYdg4co2sYV
+0F1gE5SXFGEmNTwFlpPSmKY6Zs8fKJZrzf+feCXFRlD/u+I4vftJqu7pwxZvPifR
+gPWi3kuzj71b4rEkZW3zNCP9XOtkCO/pNW2hJnc0QiTgQGvWXl/A8rIohsc+by2N
+MVr8w8iw1OdwbxI0LyC5siVgn+aER5qryYlpdeKR0/F2LuWX
+-----END CERTIFICATE-----
+private_key -----BEGIN RSA PRIVATE KEY-----
+MIIJKQIBAAKCAgEAwUg8u4KXpEL+kaPKfuEs5vImrx79ykcjqDGDr1HUZU/tik+l
+HGRnPXNES2Etcr09kLmJugnhS7PprGFP5lXSxv4ofGJQojhvbg8mYgjagPjDpMZM
+IAoU3zYHjlkK/LqNifQmld24z66LRO8HhmivyeVGA0x8bGcTE6JvrXTAgB6JfHfP
+PHKlVLx1zrYdO/zijuaKygXLMRYatCAMBETU95BsSHHzhb4ViXzVJ4oXWTI6vLvw
+gWg9zISlNkp+XuNBRiyriKBdKTMYbz5yQEYs+d7ADDLo6EMVYIiLEPQ0/KYYTSl6
+rn4zmBFFCdIcv6rsvv4uAdNEjzbDjnA/LzrM/epoWQvYpOAL89MXQSmNFGnlCz5s
+7XSVtPHRGmNQ0iQf5PKzzLuFWQBYMXhYI+G8gPVMENeGjCQmVh2WB3Bm5CY3HRRA
+tWYSAiiBzjtCkqF6cQ/cSENVFmL6HoKkgUqr3nGmH+sudpIO4W+K4I1RXQSloQQh
+idUWqp25wQafkJOQAJAsmH1KV1zZqWAe2pCFy3t4ky+dDJGok3fhhMQV4XwmL8G6
+SGbt+WwLq8Kd/p0k03icOHdju7NS+IqnUNuHxEWMBC/Hri885sbI76rJjdXB82zL
+pawo1U92TQTxYjJbaAyV8iQ7LCkzOJzqca/hcQYgqW7n3MtrQyeHm7LzMYkCAwEA
+AQKCAgBL3AhKKBVQWSMFEl4VslcnRX89WFKPo6AxEU3374wHP3mhwWSyYg3LJoR1
+eWyXDgMt3ERcCiisx649A+ySILkbdQF64DN5l+DUN4n/DC6GVBylfVa/dHWArfoF
+Opl/W9DVhkfmpiE1EfKDWbWAYXItMZlrDgf/m+z21dgzIhGzt0iK25MwzGZrfZRX
+T07mDnj1UTLD28ZGO8C7VaChxEo56Cs3u9GyekqFrcMTQ7WqQnafQLxCbiFjNeSK
+DG7Q2yzxV/LzKs2lr/I1JzM8Ws6oO27w2sJi9oFbY/wA6XgqeR4sms0V0154nr6T
+/i1eZL2KsMRp3vuXogzayN9jsBZoG3gXBE83nNK9/rXv7ExFXtlKTtzNPpJxQKYb
+YZ2LJf93vmmTYJLagTQxXHJKc2BXJJj3f09/0bXztr/gJDohTYZSuYA/c3H+ISl+
+AUZq4YI4hGOZi5e1iZYP1mUD9U42q107fXrb8HkVihaTptT2IPhYQtf+cRUpg84K
+yvAOp0VQm4xA+/NmKbV5buXYSsYh7ASTTc1LfwhsBNlsc5OUA8+EQ9GooJvS80wk
+xvsTeJ0Mml9KleY6Hw69JmZQEjbsQmLajJy4kvMQmT2NusJH/pbKrcDsazMjKqY8
+OMy+lsjUOp67mGvU7dxJC6ItkJfIpEWkJIjRUy/mF8gqSeI96QKCAQEA5YXZgsU/
+osFQZLY+qPe1tzUD/JYnwEmd4mTD/imNr+O0ZWAL8zqR8VtQsaeeQm9ktsUx3yyT
+GcXxwUiP1v4iFi9WryImD25rlGCbSNryUbf21XJec9DGptVYMxU5T1WDbfnVkX/7
+reWc8wnmhRDJ8/9rhjtlE9jUuJvs+rZt5n8Uz5t3cJqvsGTA1KcW4iU5l3nziAWj
+ZJebuZWrFgkL30cU4Py1Z1xS4tuNHeln0pF7IzKESSWFdoDB/8WBNQ1RqpscFGFn
+kPU/HirRbyT/S57v192lEHrKn0OoXketQqFqkc/xfRkVwD7bRske8/WCWcroUJl+
+dsuKGEVH3USD8wKCAQEA15Qlj2xVGJAjlf2oK6BcYLtIfBwzSc7PEojmq5u+ougS
+tyeV7NXsdbD+d95f3ZOW2b77jRe3nsWJKH5dgKoXZj5F1FbxE2KVDenAZ7OaQtml
+k1QtdNEI1v/qg4DtEmLBtYvbQK1fAsPe4PvajFYukI4SWO6/7LLKzFbKdl/0C4Qs
+QZVdFNfsBFJSYHCqkxpbhzY5t3hEK0uoVD9MEJSNPmgIHSxcnWRuMXSDVREBDEwS
+kmc+96KX4SEnn0pJ3NRQje6RhmWbb/bYNEpeFecNaAL0P9bUwYEIV3Je6bvOf9Nb
+71kouvbhRC17u36vrvMvdr9d7eg8kkch3QQVWhsfkwKCAQEAlouGsZmDNdOqUYSf
+8OAZFoP1i3VJuXwPzPDfBRRoVNf7+QpYjD78ftywPvZ8fYLnAmKxZXqtOZh2C5r2
+jcO+w+Jk7xZs9G4urfH3qH/DtQn/It2TSk/EHKWO5mKjZn/mZvoZtQfHIraajWcP
+BnSOojYEZtUKZUwxqqzLcV67ExaDpfCJFRjA5+gN+u1luwtDjTF2JN/d3hr7D201
+/IwOd3L+JNxcd+E8lIQBOX9gk+LMa7e0wO2VbrbhiEwZhZyo1khK0Kta7N+PeNAI
+8ufHc+hZ1LMSk46W3IPaKYzF/hA2AFHuSWlstN4FoZZFcSq1RwQqAMPNCUpT17uJ
+eX55NQKCAQEAuIX2IG156Sx3SUt1RuJcL/Aeex0oSWTbmeHUj88fvhEm897OVYpG
+e/aj2bZeGCrcVEVEy+AhK6WpYR/IqPjuTnW/D6Hbd9xJ+T67kggJYm8papIC1pqW
+FnG3KhiQ08v0QpETeqjrSlKd07W/u5+I+/Kfgb/aR6BCNeWUJv66xaC8wOY4Zj7r
+pkdQe3v0hTVqYrHndUNcFjMMQhBr60U8IM6rI011eMMeDvbL82Q6oWv7+ZSmMRDb
+L7hRUeckkgCpctNhfMg74/pF1XxSTC0ZLI5awsoAEiGAIlmjJC2882zWpGiMlHv9
+FX5ZCoPFnNpLJjlnDNxb/FkmgyebnyTYQQKCAQB/ef3oo/zb7OzESaIJrig52s2w
+4nyyvq6CcLZVMZ/8jN/3yU/SlzHpjdjTzS0ZFNCNPZyQKF5K9HXQAReTPBcobugc
+hlJc/EKAFxw2CZlH58qB2GMgUO0ZetHLiM+KU+AIhI/Hd+6iDUtFEduQSiWHzQth
+0F5bVH1MywUJIAXMvW4DOJetEqwHzYZ42PpJv8maWuqtaGsgv9wbDSdNy+ln5tya
+ubm4S+tIzeia5ucXFmy2xwWEOBATllxvNlBrDrwBCTgNDpJw1clo5Zz2tH1LGm/5
+G3bLC5clv3E3T/EXkst3LhcUIbRrsoQTIPeDQIyYqAurzECNCgfmyK5arNU4
+-----END RSA PRIVATE KEY-----
+private_key_type rsa
+serial_number 6e:bb:24:e5:a1:5a:15:9e:4c:07:d6:9e:4b:d0:e3:5e:57:04:bc:5b
+
예제에서는 동적 시크릿으로 PostgreSQL 데이터베이스에 대한 Database 시크릿 엔진을 구성합니다. 구성에 앞서 PostgreSQL을 설치해야 합니다. 먼저 설치될 Kubernetes Namespace를 생성합니다.
kubectl create ns postgres
+
bitnami에서 제공하는 PostgreSQL Helm Chart를 설치 합니다.
helm repo add bitnami https://charts.bitnami.com/bitnami
+
PostgreSQL을 설치 합니다.
helm upgrade --install postgres bitnami/postgresql --namespace postgres --set auth.audit.logConnections=true --set auth.postgresPassword=secret-pass
+
Kubernetes내의 Vault에서 CLI 사용
Kubernetes내에 배포된 Vault인 경우 다음과 같이 쉘을 실행할 수 있도록 Pod에 접근합니다. (Optional)
kubectl exec -it vault-0 -- /bin/sh
+
Database 시크릿 엔진을 활성화 합니다.
vault secrets enable -path=demo-db database
+
Vault에 PostgreSQL의 동적 시크릿을 위한 구성을 합니다.
PostgreSQL을 위한 롤을 생성합니다. 갱신 테스트를 위해 시크릿 수명을 1분으로 정의합니다.
vault write demo-db/roles/dev-postgres \
+ db_name=demo-db \
+ creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
+ GRANT ALL PRIVILEGES ON DATABASE postgres TO \"{{name}}\";" \
+ backend=demo-db \
+ name=dev-postgres \
+ default_ttl="1m" \
+ max_ttl="1m"
+
동적으로 생성되는 Database 계정을 확인합니다.
$ vault read demo-db/creds/dev-postgres
+
+Key Value
+--- -----
+lease_id demo-db/creds/dev-postgres/V3kcRCCQQC3loq3MCO84UnWT
+lease_duration 1m
+lease_renewable true
+password 1hmvH1a4hu-2DBrTjfzt
+username v-token-dev-post-xhRYa0CK6DFLVMqkgikF-1698388424
+
Kubernetes내의 Vault에서 CLI 사용
Kubernetes내에 배포된 Vault인 경우 다음과 같이 쉘을 실행할 수 있도록 Pod에 접근합니다. (Optional)
kubectl exec -it vault-0 -- /bin/sh
+
Vault는 Kubernetes의 Service Account 토큰으로 인증할 수 있는 Kubernetes 인증 방식을 제공합니다. CSI 드라이버가 Vault에 저장된 시크릿 정보에 접근하여 시크릿을 획득하는 과정에서 Vault에 대한 인증/인가가 요구되므로 Kubernetes상의 리소스에서는 Kubernetes 인증 방식을 통해 Kubernetes의 방식으로 인증 받는 워크플로를 구성합니다.
Vault에 Kubernetes 인증 방식을 활성화 합니다. (이미 구성된 경우 실패합니다.)
vault auth enable kubernetes
+
Kubernetes API 주소를 Kubernetes 인증 방식 구성에 설정 합니다. 이 경우 자동으로 Vault Pod를 위한 자체 Service Account를 사용합니다.
vault write auth/kubernetes/config \
+ kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443"
+
vault write auth/kubernetes/config \
+ kubernetes_host="$EXTERNAL_VAULT_ADDR"
+
생성할 Kubernetes 인증 방식의 롤 정의에서 사용되는 정책을 구성합니다. 생서한 Vault KV, PKI의 경로에 저장된 시크릿을 읽고 발행할 수 있는 정책 입니다.
vault policy write vso-app - <<EOF
+path "for-vso/data/my-pass" {
+ capabilities = ["read"]
+}
+
+path "pki/issue/my-role" {
+ capabilities = ["create", "update"]
+}
+
+path "demo-db/creds/dev-postgres" {
+ capabilities = ["read"]
+}
+EOF
+
$policy = @"
+path "for-vso/data/my-pass" {
+ capabilities = ["read"]
+}
+
+path "pki/issue/my-role" {
+ capabilities = ["create", "update"]
+}
+
+path "demo-db/creds/dev-postgres" {
+ capabilities = ["read"]
+}
+"@
+
+vault policy write vso-app - << $policy
+
예제의 롤 정의에서는 허용할 Service Account와 Kubernetes Namespace, 부여하는 정책으로 앞서 생성한 vso-app
정책을 할당합니다. 인증된 이후 유효 기간은 20분으로 설정 합니다.
vault write auth/kubernetes/role/vso \
+ bound_service_account_names=webapp-vault \
+ bound_service_account_namespaces=default \
+ policies=vso-app \
+ ttl=20m
+
VSO의 Secret 동기화를 위한 구성에 필요한 인증을 위한 리소스로 VaultAuth
를 정의해야 합니다. 다음과 같이 vault-auth-static.yaml
파일을 작성합니다.
apiVersion: secrets.hashicorp.com/v1beta1
+kind: VaultAuth
+metadata:
+ name: static-auth
+ namespace: default
+spec:
+ kubernetes:
+ audiences:
+ - vault
+ role: vso
+ serviceAccount: webapp-vault
+ tokenExpirationSeconds: 600
+ method: kubernetes
+ mount: kubernetes
+
VaultAuth
를 적용합니다.
kubectl apply -f vault-auth-static.yaml
+
시크릿 정의 중 정적 시크릿인 KV version 1, KV version 2를 정의하기위해서는 VaultStaticSecret
를 사용합니다.
VaultStaticSecret에서 정의하는 값은 다음과 같습니다.
필드 | 설명 |
---|---|
vaultAuthRef string | 지정되지 않은 경우 동일 Kubernetes Namespace의 default VaultAuth를 사용합니다. VaultAuth 리소스에 대한 VaultAuthRef에 네임스페이스 접두사를 붙일 수 있습니다(예: namespaceA/vaultAuthRefB ). |
namespace string | Vault의 Namespace를 정의합니다. |
mount string | 마운트 된 KV 시크릿 엔진 Path를 정의합니다. |
path string | 대상 KV 시크릿 엔진 내의 저장된 시크릿 경로를 정의 합니다. |
version integer | KV version 2 인 경우 저장된 시크릿 버전을 정의 합니다. |
refreshAfter string | 갱신 주기를 서정합니다. |
hmacSecretData boolean | HMACSecretData는 오퍼레이터가 시크릿 데이터의 HMAC를 계산할지 여부를 결정합니다. |
rolloutRestartTargets | 애플리케이션이 변경된 Secret의 마운트된 값을 인지하지 못하는 경우 강재로 Rollout-Restart를 수행할 대상을 정의합니다. |
destination | 저장할 Secret을 지정합니다. |
Vault의 KV version 2를 적용하는 VaultStaticSecret
정의하는 static-secret.yaml
파일 내용은 다음과 같습니다.
apiVersion: secrets.hashicorp.com/v1beta1
+kind: VaultStaticSecret
+metadata:
+ name: vault-kv-app
+ namespace: default
+spec:
+ # Static 시크릿 엔진인 KV의 유형
+ type: kv-v2
+ # KV 엔진의 마운트 Path
+ mount: for-vso
+ # KV의 시크릿 저장 경로
+ path: my-pass
+ # k8s secret 이름 정의
+ destination:
+ name: secretkv
+ create: true
+ # 정적 시크릿의 변경 확인 주기
+ refreshAfter: 30s
+ # 생성한 Vault 인증을 위한 VaultAuth 리소스 이름
+ vaultAuthRef: static-auth
+
VaultStaticSecret
을 적용합니다.
kubectl apply -f static-secret.yaml
+
적용이 완료되면 지정한 secretkv
이름으로 Secret 리소스가 생성되고, 동기화된 내용을 확인할 수 있습니다.
$ kubectl get secret
+
+NAME TYPE DATA AGE
+secretkv Opaque 2 4h55m
+
+$ kubectl get secret secretkv -o jsonpath='{.data.password}' | base64 --decode
+
+my-secret-password-v1
+
다음과 같이 저장된 시크릿을 업데이트 합니다.
$ vault kv put for-vso/my-pass password="my-secret-password-v2"
+
+==== Secret Path ====
+for-vso/data/my-pass
+
+======= Metadata =======
+Key Value
+--- -----
+created_time 2023-10-27T05:43:54.906895Z
+custom_metadata <nil>
+deletion_time n/a
+destroyed false
+version 2
+
Secret 리소스에 변경된 값이 동기화 되었는지 확인합니다.
$ kubectl get secret secretkv -o jsonpath='{.data.password}' | base64 --decode
+
+my-secret-password-v2
+
시크릿 정의 중 동적 시크릿인 PKI를 정의하기위해서는 VaultPKISecret
를 사용합니다.
VaultPKISecret에서 정의하는 값은 다음과 같습니다.
필드 | 설명 |
---|---|
vaultAuthRef string | 지정되지 않은 경우 동일 Kubernetes Namespace의 default VaultAuth를 사용합니다. VaultAuth 리소스에 대한 VaultAuthRef에 네임스페이스 접두사를 붙일 수 있습니다(예: namespaceA/vaultAuthRefB ). |
namespace string | Vault의 Namespace를 정의합니다. |
mount string | 마운트 된 KV 시크릿 엔진 Path를 정의합니다. |
role string | PKI 시크릿 엔진 구성에서 사용할 롤 이름을 정의합니다. |
revoke boolean | 정의한 리소스가 삭제되면 생성한 인증서도 취소(Revoke)할지의 여부를 정의합니다. |
clear boolean | 정의한 리소스가 삭제되면 동기화 된 Secret 리소스도 삭제할지 여부를 정의합니다. |
version integer | KV version 2 인 경우 저장된 시크릿 버전을 정의 합니다. |
expiryOffset string | 인증서를 갱신해야 하는 시기를 계산하는 데 오프셋(만료되기 이전 몇 시간 전)입니다. |
issuerRef string | PKI 발급자에 대한 참조를 정의합니다. |
rolloutRestartTargets | 애플리케이션이 변경된 Secret의 마운트된 값을 인지하지 못하는 경우 강재로 Rollout-Restart를 수행할 대상을 정의합니다. |
destination | 저장할 Secret을 지정합니다. |
commonName string | PKI 인증서 생싱시 요청할 CN 입니다. |
altNames string array | 요청에 포함할 대체 이름 DNS 이름과 이메일 주소를 나열합니다. |
ipSans string array | PKI 인증서 생싱시 요청할 IP SANs 입니다. |
uriSans string array | PKI 인증서 생싱시 요청할 URI SANs 입니다. |
otherSans string array | PKI 인증서 생싱시 요청할 oid;type:value SANs 입니다. |
ttl string | 인증서의 유효기간을 지정합니다. |
format string | 인증서 형태를 지정합니다. (pem, der, pem_bundle) |
privateKeyFormat string | 기본 값은 DER이며, 반환된 개인 키에 base64로 인코딩된 pkcs8 또는 PEM으로 인코딩된 pkcs8이 포함되도록 하려면 이 매개변수를 "pkcs8"로 지정합니다. |
notAfter string | NotAfter 필드에 날짜 값을 지정합니다. 값 형식은 UTC 형식인 YYYY-MM-ddTHH:MM:SSZ로 지정해야 합니다. |
excludeCNFromSans boolean | DNS 또는 이메일 제목 대체 이름에서 CNFromSans를 제외합니다. |
Vault의 PKI를 적용하는 VaultPKISecret
정의하는 pki-secret.yaml
파일 내용은 다음과 같습니다.
apiVersion: secrets.hashicorp.com/v1beta1
+kind: VaultPKISecret
+metadata:
+ name: vault-pki-app
+ namespace: default
+spec:
+ # PKI 시크릿 엔진의 마운트 Path
+ mount: pki
+ # 인증서를 발급할 PKI 롤
+ role: my-role
+ # 옵션
+ commonName: test.example.com
+ # 인증서 형태
+ format: pem
+ # 갱신 트리거 시간
+ expiryOffset: 2s
+ # 발급되는 인증서의 TTL
+ ttl: 30s
+ # k8s secret 이름 정의
+ destination:
+ name: secretpki
+ create: true
+ # 교체 발생시 Restart할 대상 지정
+ rolloutRestartTargets:
+ - kind: Deployment
+ name: vso-pki-demo
+ # 생성한 Vault 인증을 위한 VaultAuth 리소스 이름
+ vaultAuthRef: static-auth
+
VaultPKISecret
을 적용합니다.
kubectl apply -f pki-secret.yaml
+
적용이 완료되면 지정한 secretpki
이름으로 Secret 리소스가 생성되고, 동기화된 내용을 확인할 수 있습니다. 여기서는 UNIXTIMESTAMP로 기록되는 expiration으로 확인해 봅니다.
$ kubectl get secret
+
+NAME TYPE DATA AGE
+secretpki Opaque 2 4h55m
+
+$ kubectl get secret secretpki -o jsonpath='{.data.expiration}' | base64 --decode
+
+1698386116
+
+### 시간이 흐른 뒤 새로 발급된 인증서의 만료 시간을 확인합니다.
+
+$ kubectl get secret secretpki -o jsonpath='{.data.expiration}' | base64 --decode
+
+1698386172
+
시크릿 정의 중 동적 시크릿인 PKI를 정의하기위해서는 VaultDynamicSecret
를 사용합니다.
VaultDynamicSecret에서 정의하는 값은 다음과 같습니다.
필드 | 설명 |
---|---|
vaultAuthRef string | 지정되지 않은 경우 동일 Kubernetes Namespace의 default VaultAuth를 사용합니다. VaultAuth 리소스에 대한 VaultAuthRef에 네임스페이스 접두사를 붙일 수 있습니다(예: namespaceA/vaultAuthRefB ). |
namespace string | Vault의 Namespace를 정의합니다. |
mount string | 마운트 된 KV 시크릿 엔진 Path를 정의합니다. |
requestHTTPMethod string | Vault에서 시크릿을 동기화할 때 사용할 요청 HTTPMethod이며, 기본은 GET 이므로 필요시 다른 요청 HTTPMethod를 정의합니다. |
path string | Vault에서 자격 증명을 가져올 경로이며, 마운트에 상대적입니다. |
clear boolean | 정의한 리소스가 삭제되면 동기화 된 Secret 리소스도 삭제할지 여부를 정의합니다. |
params object | 요청할 때 전달하는 매개변수를 정의합니다. |
renewalPercent integer | 갱신을 위한 지점을 정의하며, 기본 값은 67 % 입니다. |
revoke boolean | 정의한 리소스가 삭제되면 생성한 인증서도 취소(Revoke)할지의 여부를 정의합니다. |
allowStaticCreds boolean | 요청 시 생성하는 것이 아니라 Vault 서버에서 주기적으로 회전하는 자격 증명을 동기화할 때 AllowStaticCreds를 설정해야 합니다. |
rolloutRestartTargets | 애플리케이션이 변경된 Secret의 마운트된 값을 인지하지 못하는 경우 강재로 Rollout-Restart를 수행할 대상을 정의합니다. |
destination | 저장할 Secret을 지정합니다. |
Vault의 Database 시크릿 엔진의 동적 시크릿을 적용하는 VaultDynamicSecret
정의하는 dynamic-secret.yaml
파일 내용은 다음과 같습니다.
apiVersion: secrets.hashicorp.com/v1beta1
+kind: VaultDynamicSecret
+metadata:
+ name: vault-db-app
+ namespace: default
+spec:
+ # 활성화 된 시크릿 엔진 Path
+ mount: demo-db
+ # 시크릿 롤 경로
+ path: creds/dev-postgres
+ # k8s secret 이름 정의
+ destination:
+ create: true
+ name: secretdb
+ # 교체 발생시 Restart할 대상 지정
+ rolloutRestartTargets:
+ - kind: Deployment
+ name: vso-db-demo
+ # 생성한 Vault 인증을 위한 VaultAuth 리소스 이름
+ vaultAuthRef: static-auth
+
$ kubectl get secret
+
+NAME TYPE DATA AGE
+secretdb Opaque 3 2h55m
+
+$ kubectl get secret secretdb -o jsonpath='{.data.username}' | base64 --decode
+
+v-kubernet-dev-post-Pl4QC4UC6rQ8uThG4pR8-169838984
+
+### 시간이 흐른 뒤 새로 발급된 인증서의 만료 시간을 확인합니다.
+
+$ kubectl get secret secretdb -o jsonpath='{.data.username}' | base64 --decode
+
+v-kubernet-dev-post-Dvta1R6q5bV9zrlFb5Zn-1698389887
+
Kv 추가
$ vault secrets enable -version=2 -path=systemcreds/ kv
+
권한 추가
$ vault policy write rotate-windows - << EOF
+# Allows hosts to write new passwords
+path "systemcreds/data/windows/*" {
+ capabilities = ["create", "update"]
+}
+# Allow hosts to generate new passphrases
+path "gen/passphrase" {
+ capabilities = ["update"]
+}
+# Allow hosts to generate new passwords
+path "gen/password" {
+ capabilities = ["update"]
+}
+EOF
+
+$ vault policy write windowsadmin - << EOF
+# Allows admins to read passwords.
+path "systemcreds/*" {
+ capabilities = ["list"]
+}
+path "systemcreds/data/windows/*" {
+ capabilities = ["list", "read"]
+}
+EOF
+
토큰
$ vault token create -policy=rotate-windows -period=600h
+
사용자
$ vault auth enable userpass
+$ vault write auth/userpass/users/pwadmin password=password policies=windowsadmin
+
PowerShell e.g.
$VAULT_ADDR = $env:USERNAME
+# Make sure the user exists on the local system.
+if (-not (Get-LocalUser $USERNAME)) {
+ throw '$USERNAME does not exist!'
+}
+
+# Use TLS
+# [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+
+# Import some environment variables.
+$VAULT_ADDR = $env:VAULT_ADDR
+$VAULT_TOKEN = $env:VAULT_TOKEN
+$HOSTNAME = $env:computername
+
+# Renew our token before we do anything else.
+Invoke-RestMethod -Headers @{"X-Vault-Token" = ${VAULT_TOKEN}} -Method POST -Uri ${VAULT_ADDR}/v1/auth/token/renew-self
+if(-Not $?)
+{
+ Write-Output "Error renewing Vault token lease."
+}
+
+# Fetch a new passphrase from Vault. Adjust the options to fit your requirements.
+#$NEWPASS = (Invoke-RestMethod -Headers @{"X-Vault-Token" = ${VAULT_TOKEN}} -Method POST -Body "{`"words`":`"4`",`"separator`":`"-`"}" -Uri ${VAULT_ADDR}/v1/gen/passphrase).data.value
+
+# Fetch a new password from Vault. Adjust the options to fit your requirements.
+$NEWPASS = c:\hashicorp\nomad\nomad operator keygen
+
+# Convert into a SecureString
+$SECUREPASS = ConvertTo-SecureString $NEWPASS -AsPlainText -Force
+
+# Create the JSON payload to write to Vault's K/V store. Keep the last 12 versions of this credential.
+$JSON="{ `"options`": { `"max_versions`": 12 }, `"data`": { `"$USERNAME`": `"$NEWPASS`" } }"
+
+# First commit the new password to vault, then change it locally.
+Invoke-RestMethod -Headers @{"X-Vault-Token" = ${VAULT_TOKEN}} -Method POST -Body $JSON -Uri ${VAULT_ADDR}/v1/systemcreds/data/windows/${HOSTNAME}/${USERNAME}_creds
+if($?) {
+ Write-Output "Vault updated with new password."
+ $UserAccount = Get-LocalUser -name $USERNAME
+ $UserAccount | Set-LocalUser -Password $SECUREPASS
+ if($?) {
+ Write-Output "${USERNAME}'s password was stored in Vault and updated locally."
+ }
+ else {
+ Write-Output "Error: ${USERNAME}'s password was stored in Vault but *not* updated locally."
+ }
+}
+else {
+ Write-Output "Error saving new password to Vault. Local password will remain unchanged."
+}
+
Job e.g.
job "pw-update" {
+ datacenters = ["hashistack"]
+ type = "batch"
+
+ constraint {
+ attribute = "${meta.target}"
+ value = "windows2016"
+ }
+
+ periodic {
+ cron = "0 */5 * * * * *"
+ prohibit_overlap = true
+ time_zone = "Asia/Seoul"
+ }
+
+ group "pw-update" {
+ count = 1
+ task "powershell" {
+ driver = "raw_exec"
+ config {
+ command = "powershell.exe"
+ args = ["-noprofile", "-executionpolicy", "bypass", "-file", "local/pw-rotate.ps1"]
+ }
+ env {
+ VAULT_TOKEN = "s.EZFCRJhNmjSc9U5b4EX5gwyy"
+ VAULT_ADDR = "http://172.28.128.21:8200"
+ USERNAME = "testuser"
+ }
+ template {
+data = <<EOF
+$USERNAME = $env:USERNAME
+# Make sure the user exists on the local system.
+if (-not (Get-LocalUser $USERNAME)) {
+ throw '$USERNAME does not exist!'
+}
+
+# Use TLS
+# [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+
+# Import some environment variables.
+$VAULT_ADDR = $env:VAULT_ADDR
+$VAULT_TOKEN = $env:VAULT_TOKEN
+$HOSTNAME = $env:computername
+
+# Renew our token before we do anything else.
+Invoke-RestMethod -Headers @{"X-Vault-Token" = ${VAULT_TOKEN}} -Method POST -Uri ${VAULT_ADDR}/v1/auth/token/renew-self
+if(-Not $?)
+{
+ Write-Output "Error renewing Vault token lease."
+}
+
+# Fetch a new password from Vault. Adjust the options to fit your requirements.
+$NEWPASS = c:\hashicorp\nomad\nomad operator keygen
+
+# Convert into a SecureString
+$SECUREPASS = ConvertTo-SecureString $NEWPASS -AsPlainText -Force
+
+# Create the JSON payload to write to Vault's K/V store. Keep the last 12 versions of this credential.
+$JSON="{ `"options`": { `"max_versions`": 12 }, `"data`": { `"$USERNAME`": `"$NEWPASS`" } }"
+
+# First commit the new password to vault, then change it locally.
+Invoke-RestMethod -Headers @{"X-Vault-Token" = ${VAULT_TOKEN}} -Method POST -Body $JSON -Uri ${VAULT_ADDR}/v1/systemcreds/data/windows/${HOSTNAME}/${USERNAME}_creds
+if($?) {
+ Write-Output "Vault updated with new password."
+ $UserAccount = Get-LocalUser -name $USERNAME
+ $UserAccount | Set-LocalUser -Password $SECUREPASS
+ if($?) {
+ Write-Output "${USERNAME}'s password was stored in Vault and updated locally."
+ }
+ else {
+ Write-Output "Error: ${USERNAME}'s password was stored in Vault but *not* updated locally."
+ }
+}
+else {
+ Write-Output "Error saving new password to Vault. Local password will remain unchanged."
+}
+EOF
+ destination = "local/pw-rotate.ps1"
+ }
+ }
+ }
+}
+
+
+
+
Vault HTTP Status Codes : https://www.vaultproject.io/api#http-status-codes
Vault에 API 요청시 400에러가 발생하는 경우 Vault로 전달된 데이터 형태가 올바른지 확인이 필요하다.
400
: Invalid request, missing or invalid data.예를들어 아래와 같이 Transit
의 복호화 요청을 하는 경우 데이터가 비어있다면 응답과 Audit로그에서 400 에러관련 메세지를 확인할 수 있다.
1 error occurred: * invalid request
curl \
+ -H "X-Vault-Token: s.HeeRXjkW1KJhF8ofQsglI9yw" \
+ -X POST \
+ -d "{}" \
+ http://192.168.60.103:8200/v1/transit/decrypt/my-key
+
{
+ "time": "2022-03-04T08:02:37.596190958Z",
+ "type": "response",
+ "auth": {
+ "client_token": "hmac-sha256:17bc16e3346dd6c398646cb7da8e0bd71ae720f608a8c447b8942b8283388600",
+ "accessor": "hmac-sha256:798bb09d10dc2ac18533acb3d049c4185af3328fbc88fedd23081f63caa13b44",
+ "display_name": "root",
+ "policies": [
+ "root"
+ ],
+ "token_policies": [
+ "root"
+ ],
+ "token_type": "service",
+ "token_issue_time": "2021-09-15T13:45:47+09:00"
+ },
+ "request": {
+ "id": "c39906c5-f48d-c177-37c2-4c1635db78e7",
+ "operation": "update",
+ "mount_type": "transit",
+ "client_token": "hmac-sha256:17bc16e3346dd6c398646cb7da8e0bd71ae720f608a8c447b8942b8283388600",
+ "client_token_accessor": "hmac-sha256:798bb09d10dc2ac18533acb3d049c4185af3328fbc88fedd23081f63caa13b44",
+ "namespace": {
+ "id": "root"
+ },
+ "path": "kbhealth-transit/prod/decrypt/aes256",
+ "data": {
+ "ciphertext": "hmac-sha256:34c2966e2ef36e2dcdb24f05fd4442b8f85c0d2fbf0887977636c7592e2cef3b"
+ },
+ "remote_address": "10.100.0.85"
+ },
+ "response": {
+ "mount_type": "transit",
+ "data": {
+ "error": "hmac-sha256:bf7d730e400653f79b134c3bdb593f8220f6f1588a26048a6e1272a01ad47384"
+ }
+ },
+ "error": "1 error occurred:\n\t* invalid request\n\n"
+}
+
현상 : $vault read mysql/creds/my-role 입력시 오류
오류 내용 :
Error reading mysql/creds/my-role: Error making API request.
+URL: GET http://127.0.0.1:8200/v1/mysql/creds/my-role
+Code: 500. Errors:
+
+* 1 error occurred:
+ * Error 1470: String 'v-root-my-role-87BP93fheiaHKGelc' is too long for user name (should be no longer than 16)
+
원인 : mysql 버전이 오래되어 mysql-database-plugin이 아닌 mysql-legacy-database-plugin 을 사용해야 함.
https://github.com/hashicorp/vault/issues/4602
해결방안 : vault에서 database에 접근하기 위한 plugin설정시 아래와 같이 설정을 변경해야 함.
$ vault write mysql/config/mysql-database \
+ plugin_name=mysql-database-plugin \
+ connection_url="{{username}}:{{password}}@tcp(192.168.56.204:3306)/" \
+ allowed_roles="my-role" \
+ username="vault2" \
+ password="vaultpass"
+
$ vault write mysql/config/mysql-database \
+ plugin_name=mysql-legacy-database-plugin \
+ connection_url="{{username}}:{{password}}@tcp(192.168.56.204:3306)/" \
+ allowed_roles="my-role" \
+ username="vault2" \
+
# consul tls create로 인증서 생성
+consul tls ca create -domain=vault -days 3650
+consul tls cert create -domain=vault -dc=global -server -days 3650
+consul tls cert create -domain=vault -dc=global -client -days 3650
+consul tls cert create -domain=vault -dc=global -cli -days 3650
+
+# vault config는 아래와 같다.
+ui = true
+
+storage "consul" {
+ address = "127.0.0.1:8500"
+ path = "vault/"
+}
+
+listener "tcp" {
+ address = "0.0.0.0:8200"
+ #tls_disable = 1
+ tls_cert_file = "/root/temp/global-server-vault-0.pem"
+ tls_key_file = "/root/temp/global-server-vault-0-key.pem"
+}
+
+disable_mlock = true
+default_lease_ttl = "768h"
+max_lease_ttl = "768h"
+
+api_addr = "https://172.21.2.50:8200"
+
+# 명령어를 써야 할 경우 cli 인증서를 export 해줘야한다.
+export VAULT_CACERT="${HOME}/temp/vault-agent-ca.pem"
+export VAULT_CLIENT_CERT="${HOME}/temp/global-cli-vault-0.pem"
+export VAULT_CLIENT_KEY="${HOME}/temp/global-cli-vault-0-key.pem"
+
참고 URL : https://learn.hashicorp.com/tutorials/vault/agent-aws
$ sw_vers
+ProductName: macOS
+ProductVersion: 12.4
+
+$ vault version
+Vault v1.11.0
+
vault server -dev -dev-root-token-id=root
+
Another terminal
export VAULT_ADDR=http://127.0.0.1:8200
+export VAULT_TOKEN=root
+
export AWS_ACCESS_KEY=AKIAU3NXXXXX
+export AWS_SECRET_KEY=Rex3GPUKO3++123
+export AWS_REGION=ap-northeast-2
+
vault secrets enable aws
+
vault write aws/config/root \
+ access_key=$AWS_ACCESS_KEY \
+ secret_key=$AWS_SECRET_KEY \
+ region=$AWS_REGION
+
vault write /aws/config/lease lease=1m lease_max=1m
+
vault write aws/roles/s3 \
+ credential_type=iam_user \
+ policy_document=-<<EOF
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:PutObject",
+ "s3:PutObjectAcl"
+ ],
+ "Resource": "*"
+ }
+ ]
+}
+EOF
+
$ vault write -force aws/creds/s3
+Key Value
+--- -----
+lease_id aws/creds/s3/w5hPWazlpWu4jKHUyR0WBnbZ
+lease_duration 1m
+lease_renewable true
+access_key AKIAU3NXDWRUCYVDBEYM
+secret_key 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
+security_token <nil>
+
cat <<EOF | vault policy write aws_policy -
+path "aws/creds/s3" {
+ capabilities = ["read","update"]
+}
+EOF
+
$ vault auth enable approle
+
+Success! Enabled approle auth method at: approle/
+
+$ vault write auth/approle/role/aws-cred \
+ secret_id_ttl=120m \
+ token_ttl=60m \
+ token_max_ttl=120m \
+ policies="aws_policy"
+
+Success! Data written to: auth/approle/role/aws-cred
+
+$ vault read auth/approle/role/aws-cred/role-id
+
+Key Value
+--- -----
+role_id 430111ee-5955-aa83-a53d-924b7e11ac36
+
+$ vault write -f auth/approle/role/aws-cred/secret-id
+
+Key Value
+--- -----
+secret_id 7f86b671-2f47-f841-18a4-c36ca34ab8d8
+secret_id_accessor 9ad4256a-acc6-e8c0-f7fe-7633e66b1318
+secret_id_ttl 2h
+
role_id
는 Pipeline 작성에 사용$ cat <<EOF > ./vault-agent-config.hcl
+pid_file = "./pidfile"
+
+listener "tcp" {
+ address = "127.0.0.1:9200"
+ tls_disable = true
+ # tls_cert_file = "/path/to/cert.pem"
+ # tls_key_file = "/path/to/key.pem"
+}
+
+cache {
+ use_auto_auth_token = true
+}
+
+auto_auth {
+ method {
+ type = "approle"
+ config = {
+ role_id_file_path = "./role_id.txt"
+ secret_id_file_path = "./secret_id.txt"
+ }
+ }
+}
+
+vault {
+ address = "http://127.0.0.1:8200"
+}
+
+# template 은 여러개 설정 가능
+template {
+ source = "./aws_cred.tpl"
+ destination = "./aws_cred.txt"
+ command = "cat ./aws_cred.txt" # 생략 가능
+}
+EOF
+
$ cat <<EOF > ./aws_cred.tpl
+{{- with secret "aws/creds/s3" -}}
+aws_access_key={{ .Data.access_key | toJSON }}
+aws_secret_key={{ .Data.secret_key | toJSON }}
+{{- end }}
+EOF
+
$ vault read -field role_id auth/approle/role/aws-cred/role-id > ./role_id.txt
+
+$ vault write -f -field secret_id auth/approle/role/aws-cred/secret-id > ./secret_id.txt
+
+$ vault agent -config=./vault-agent-config.hcl
+==> Vault agent started! Log data will stream in below:
+
+==> Vault agent configuration:
+
+ Api Address 1: http://127.0.0.1:9200
+ Api Address 2: http://bufconn
+ Cgo: disabled
+ Log Level: info
+ Version: Vault v1.11.0, built 2022-06-17T15:48:44Z
+ Version Sha: ea296ccf58507b25051bc0597379c467046eb2f1
+
+2022-06-30T17:17:08.726+0900 [INFO] sink.file: creating file sink
+2022-06-30T17:17:08.727+0900 [INFO] sink.file: file sink configured: path=/tmp/vault_agent mode=-rw-r-----
+2022-06-30T17:17:08.728+0900 [INFO] sink.server: starting sink server
+2022-06-30T17:17:08.730+0900 [INFO] template.server: starting template server
+2022-06-30T17:17:08.730+0900 [INFO] auth.handler: starting auth handler
+2022-06-30T17:17:08.730+0900 [INFO] auth.handler: authenticating
+2022-06-30T17:17:08.736+0900 [INFO] (runner) creating new runner (dry: false, once: false)
+2022-06-30T17:17:08.739+0900 [INFO] auth.handler: authentication successful, sending token to sinks
+2022-06-30T17:17:08.740+0900 [INFO] auth.handler: starting renewal process
+2022-06-30T17:17:08.740+0900 [INFO] sink.file: token written: path=/tmp/vault_agent
+2022-06-30T17:17:08.743+0900 [INFO] (runner) creating watcher
+2022-06-30T17:17:08.745+0900 [INFO] auth.handler: renewed auth token
+2022-06-30T17:17:08.745+0900 [INFO] template.server: template server received new token
+2022-06-30T17:17:08.745+0900 [INFO] (runner) stopping
+2022-06-30T17:17:08.745+0900 [INFO] (runner) creating new runner (dry: false, once: false)
+2022-06-30T17:17:08.745+0900 [INFO] (runner) creating watcher
+2022-06-30T17:17:08.747+0900 [INFO] (runner) starting
+2022-06-30T17:17:11.635+0900 [INFO] (runner) rendered "./aws_cred.tpl" => "./aws_cred.txt"
+2022-06-30T17:17:11.635+0900 [INFO] (runner) executing command "[\"cat ./aws_cred.txt\"]" from "./aws_cred.tpl" => "./aws_cred.txt"
+2022-06-30T17:17:11.637+0900 [INFO] (child) spawning: sh -c cat ./aws_cred.txt
+aws_access_key="AKIAU3NXDWRUHRF5SJDM"
+aws_secret_key="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"
+2022-06-30T17:17:54.978+0900 [WARN] vault.read(aws/creds/s3): TTL of "1m" exceeded the effective max_ttl of "17s"; TTL value is capped accordingly
+2022-06-30T17:17:54.978+0900 [WARN] vault.read(aws/creds/s3): renewer done (maybe the lease expired)
+2022-06-30T17:17:57.422+0900 [INFO] (runner) rendered "./aws_cred.tpl" => "./aws_cred.txt"
+2022-06-30T17:17:57.422+0900 [INFO] (runner) executing command "[\"cat ./aws_cred.txt\"]" from "./aws_cred.tpl" => "./aws_cred.txt"
+2022-06-30T17:17:57.422+0900 [INFO] (child) spawning: sh -c cat ./aws_cred.txt
+aws_access_key="AKIAU3NXDWRUETMPDDOH"
+aws_secret_key="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"
+
$ watch cat ./aws_cred.txt
+Every 2.0s: cat ./aws_cred.txt gs-C02CT3ZFML85: Thu Jun 30 17:19:01 2022
+
+aws_access_key="AKIAU3NXDWRUHRF5SJDM"
+aws_secret_key="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"
+
+<< expire 되면 다시 발급 됨 >>
+
+aws_access_key="AKIAU3NXDWRUOOG33CPC"
+aws_secret_key="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"
+
Vault Agent로는 X-Vault-Token 없이 API 호출 가능
$ curl -X GET http://127.0.0.1:9200/v1/aws/creds/s3 | jq '.data'
+{
+ "access_key": "AKIAU3NXDWRUJJ4UW2P2",
+ "secret_key": "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",
+ "security_token": null
+}
+
[Unit]
+Description=Nomad Agent
+Requires=consul-online.target
+After=consul-online.target
+
+[Service]
+KillMode=process
+KillSignal=SIGINT
+Environment=VAULT_ADDR=http://active.vault.service.consul:8200
+Environment=VAULT_SKIP_VERIFY=true
+ExecStartPre=/usr/local/bin/vault agent -config /etc/vault-agent.d/vault-agent.hcl
+ExecStart=/usr/bin/nomad-vault.sh
+ExecReload=/bin/kill -HUP $MAINPID
+Restart=on-failure
+RestartSec=2
+StartLimitBurst=3
+StartLimitIntervalSec=10
+LimitNOFILE=65536
+
+[Install]
+WantedBy=multi-user.target
+
License
측정 시점 : 2022-12-9
Case 1 : Vault 바이너리가 릴리즈된 시점이 expiration 기간보다 이전인 경우
실행 가능
사용은 불가
2022-12-09T14:39:09.474+0900 [ERROR] core.licensing: core: licensing error: expiration_time="2022-06-19 00:00:00 +0000 UTC" time_left=-4157h39m9s
+
Case 2 : Vault 바이너리가 릴리즈된 시점이 expiration 기간보다 이후인 경우
실행 불가
Error initializing core: licensing could not be initialized: license validation failed: 1 error occurred:
+ * license expiration date is before version build date
+
Case 3 : 평가 라이선스의 Termination 시점 이후
Case 3 : 일반 라이선스의 Termination 시점 이후
expiration되었지만 실행은 가능한 이유
/sys/config/reload/license
로 변경된 환경변수 또는 파일을 다시 읽음 license_path
로 지정된 파일 경로 입력VAULT_LICENSE
에 라이선스 내용 입력VAULT_LICENSE_PATH
에 라이선스 파일 경로 입력import "strings"
+
+# print(request.data)
+credential_type = request.data.credential_type
+print("CREDENTIAL_TYPE: ", credential_type)
+
+allow_role_type = ["federation_token"]
+
+role_type_check = rule {
+ credential_type in allow_role_type
+}
+
+# Only check AWS Secret Engine
+# Only check create, update
+precond = rule {
+ request.operation in ["create", "update"]
+}
+
+main = rule when precond {
+ role_type_check
+}
+
precond
: API 요청이 POST, UPDATE 인 경우role_type_check
: 요청의 Body에 credential_type
의 값이 허용된 type 인지 확인 (e.g. federation_token
허용)EGP는 지정된 path에 대해 정책을 적용
$ vault write /sys/policies/egp/iam_user_deny \
+ policy=@egp_iam_user_deny.sentinel \
+ enforcement_level=hard-mandatory \
+ paths="aws/roles/*"
+
paths
로 지정된 API 경로에 요청이 들어오면 동작EGP로 지정된 path로 credential_type 이 iam_user 인경우 허용된 타입이 아니므로 거부됨
$ vault write aws/roles/iam-role \
+ credential_type=iam_user \
+ policy_document=-<<EOF
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": "ec2:*",
+ "Resource": "*"
+ }
+ ]
+}
+EOF
+
에러 메시지
Error writing data to aws/roles/iam-role: Error making API request.
+
+URL: PUT http://127.0.0.1:8200/v1/aws/roles/iam-role
+Code: 403. Errors:
+
+* 2 errors occurred:
+ * egp standard policy "root/iam_user_deny" evaluation resulted in denial.
+
+The specific error was:
+<nil>
+
+A trace of the execution for policy "root/iam_user_deny" is available:
+
+Result: false
+
+Description: <none>
+
+print() output:
+
+CREDENTIAL_TYPE: iam_user
+
+
+Rule "main" (root/iam_user_deny:19:1) = false
+Rule "precond" (root/iam_user_deny:15:1) = true
+Rule "role_type_check" (root/iam_user_deny:9:1) = false
+ * permission denied
+
federation_token
은 생성됩니다.
$ vault write aws/roles/sts-role \
+ credential_type=federation_token \
+ policy_document=-<<EOF
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": "ec2:*",
+ "Resource": "*"
+ }
+ ]
+}
+EOF
+Success! Data written to: aws/roles/sts-role
+
import "strings"
+
+exportable = request.data.exportable
+
+exportable_check = rule {
+ exportable is "false"
+}
+
+main = rule {
+ exportable_check
+}
+
exportable_check
: 요청의 Body에 exportable
의 값이 false
인 경우에 TRUE 반환EGP는 지정된 path에 대해 정책을 적용
$ vault write sys/policies/egp/exportable-check \
+ policy="$(base64 -i exportable-deny.sentinel)" \
+ paths="*" \
+ enforcement_level="hard-mandatory"
+
paths
로 지정된 API 경로에 요청이 들어오면 동작transit
경로에 모든 권한이 있는 사용자
vault policy write transit-admin - << EOF
+path "transit" {
+capabilities = ["create", "read", "update", "delete", "list"]
+}
+
+path "transit/*" {
+capabilities = ["create", "read", "update", "delete", "list"]
+}
+EOF
+
VAULT_TOKEN=$(vault token create -policy=transit-admin)
+
exportable
옵션이 false인
경우 정상적으로 수행
$ vault write -f transit/keys/my-key-name exportable=false
+
+Key Value
+--- -----
+allow_plaintext_backup false
+auto_rotate_period 0s
+deletion_allowed false
+derived false
+exportable false
+imported_key false
+keys map[1:1702877441]
+latest_version 1
+min_available_version 0
+min_decryption_version 1
+min_encryption_version 0
+name my-key-name
+supports_decryption true
+supports_derivation true
+supports_encryption true
+supports_signing false
+type aes256-gcm96
+
exportable
옵션이 false인
경우 거부됨
$ vault write -f transit/keys/my-key-name exportable=true
+
+Error writing data to transit/keys/my-key-name: Error making API request.
+
+URL: PUT http://127.0.0.1:8200/v1/transit/keys/my-key-name
+Code: 403. Errors:
+
+* 2 errors occurred:
+ * egp standard policy "root/exportable-check" evaluation resulted in denial.
+
+The specific error was:
+<nil>
+
+A trace of the execution for policy "root/exportable-check" is available:
+
+Result: false
+
+Description: <none>
+
+Rule "main" (root/exportable-check:9:1) = false
+Rule "exportable_check" (root/exportable-check:5:1) = false
+ * permission denied
+
+
+
https://learn.hashicorp.com/tutorials/nomad/production-reference-architecture-vm-with-consul
Nomad는 Server/Client 구조로 구성되며, Client의 경우 자원사용량이 매우 미미하므로 자원산정은 Server를 기준으로 산정
Size | CPU | Memory | Disk Capacity | Typical Cloud Instance Types |
---|---|---|---|---|
최소 | 2-4 core | 8-16 GB RAM | 50 GB | AWS: m5.large, m5.xlarge |
Azure: Standard_D2_v3, Standard_D4_v3 | ||||
GCP: n2-standard-2, n2-standard-4 | ||||
권장 | 8-16 core | 32-64 GB RAM | 100 GB | AWS: m5.2xlarge, m5.4xlarge |
Azure: Standard_D8_v3, Standard_D16_v3 | ||||
GCP: n2-standard-8, n2-standard-16 |
원문 : https://www.hashicorp.com/blog/resilient-infrastructure-with-nomad-restarting-tasks
Nomad가 종종 운영자 개입 없이 장애, 중단 상황, Nomad 클러스터 인프라의 유지 관리를 처리하는 방법 확인
Job
은 실행 드라이버(Docker, Java, Exec 등)에 의해 Nomad 클라이언트 노드 에서 실행되는 명령, 서비스, 애플리케이션, 배치와 같은 형태입니다. task
에서는 웹 애플리케이션, 데이터베이스 서버 또는 API와 같은 단기 배치 작업 또는 장기적으로 실행되는 서비스의 실행 방식을 정의 합니다.
redis를 실행하는 Job의 예는 다음과 같습니다.
job "example" {
+ datacenters = ["dc1"]
+ type = "service"
+
+ constraint {
+ attribute = "${attr.kernel.name}"
+ value = "linux"
+ }
+ group "cache" {
+ count = 1
+
+ task "redis" {
+ driver = "docker"
+
+ config {
+ image = "redis:3.2"
+ }
+ resources {
+ cpu = 500 # 500 MHz
+ memory = 256 # 256MB
+ }
+ }
+ }
+}
+
Job을 작성할 때 작성자는 워크로드에 대한 리소스와 제약 조건을 정의할 수 있습니다. 제약 조건 은 커널 유형, 버전, OS와 같은 속성에 따라 노드의 워크로드 배치를 지정 및 제한합니다. 리소스 요구 사항에는 task 실행에 필요한 메모리, 네트워크, CPU 등이 포함됩니다.
Nomad는 Job 작성자가 실패하고 응답하지 않는 task 동작을 자동으로 다시 시작하고, 반복적으로 실패한 작업들을 다른 노드로 자동으로 재예약하는 전략을 지정할 수 있도록 하여 워크로드를 탄력적으로 만듭니다.
Job의 실행 실패는 성공적으로 완료되지 않거나 오류 또는 리소스(CPU, Memory) 부족으로 인해 서비스가 실패하는 경우 발생할 수 있습니다.
Nomad는 Job 파일의 restart
절에 있는 지시문에 따라 동일한 노드에서 실패한 task를 다시 시작합니다. 운영자는 재시작 횟수인 attempts
, 지연된 작업을 재시작하기 전에 Nomad가 기다려야 하는 시간인 delay
, 시도된 재시작을 간격으로 제한하는 interval
시간을 지정합니다. failure
인 mode
를 사용하여 주어진 간격 내의 모든 재시작 시도가 소진된 후 task가 실행되지 않는 경우 Nomad가 수행해야 할 동작을 지정합니다.
기본 실패 모드는 Nomad가 Job을 다시 시작하지 않도록 지시하는 fail
입니다. 이것은 몇 번의 실패 후에 성공할 가능성이 없는 멱등성이 아닌 실행 방식에 권장되는 값입니다. 다른 옵션은 Job을 다시 시작하기 전에 간격(interval)으로 지정된 시간만큼 기다리도록 Nomad에 지시하는 지연(delay)입니다.
다음 재시작 스탠자인 restart
는 Nomad에게 30분 이내에 최대 2번의 재시작을 시도하도록 지시하고, 각 재시작 사이에 15초를 지연시키고 모두 소진된 후에는 더 이상 재시작을 시도하지 않습니다.
group "cache" {
+ ...
+ restart {
+ attempts = 2
+ interval = "30m"
+ delay = "15s"
+ mode = "fail"
+ }
+ task "redis" {
+ ...
+ }
+}
+
다시 restart
동작은 버그, 메모리 누수, 기타 일시적인 문제에 대해 작업을 복원할 수 있도록 설계되었습니다. 이는 Nomad 외부에서 systemd, upstart 또는 runit과 같은 프로세스 관리자를 사용하는 것과 유사합니다.
또 다른 일반적인 시나리오는 프로세스 실행은 실패하지 않았지만 응답하지 않거나 비정상이 된 task를 다시 시작해야 하는 것입니다. Nomad는 check_restart
스탠자의 지시에 따라 응답하지 않는 Job을 다시 시작합니다. 이 동작은 Consul Health Check와 함께 작동합니다. 상태 확인이 제한 시간(limit)에 실패하면 Nomad가 Job을 재시작합니다. 값이 1
이면 첫 번째 실패 시 다시 시작됩니다. 기본값인 0
은 상태 확인 기반 재시작을 비활성화합니다.
실패는 연속적으로 발생해야 합니다. 한번이라도 정상적이였다면 카운트를 재설정하므로 통과 상태와 실패 상태를 번갈아 가는 경우 서비스가 다시 시작되지 않을 수 있습니다. 다시 시작한 후 상태 확인을 재개하기 위해 대기 기간을 지정하려면 grace
를 사용하십시오. Nomad가 경고 상태를 통과 상태로 처리하고 다시 시작을 트리거하지 않도록 하려면 ignore_warnings = true
로 설정하십시오.
다음 check_restart
정책은 상태 확인이 연속 3회 실패한 후 Redis 작업을 다시 시작하도록 Nomad에 지시합니다. task를 다시 시작한 후 90초 동안 대기하여 상태 확인을 재개하고 경고 상태(실패 포함)에서 다시 시작합니다.
task "redis" {
+ ...
+ service {
+ check_restart {
+ limit = 3
+ grace = "90s"
+ ignore_warnings = false
+ }
+ }
+}
+
기존 데이터 센터 환경에서 실패한 task를 다시 시작하는 것은 종종 작업자가 구성해야 하는 프로세스 관리자가 처리합니다. 비정상 task를 자동으로 감지하고 다시 시작하는 것은 더 복잡하며 모니터링 시스템이나 운영자 개입을 통합하기 위한 사용자 지정 스크립트가 필요합니다. Nomad를 사용하면 운영자 개입 없이 자동으로 발생합니다.
지정된 수의 다시 시작한 후에 성공적으로 실행되지 않는 task는 하드웨어 오류, 커널 교착 상태 또는 기타 복구할 수 없는 오류와 같이 실행 중인 노드의 문제로 인해 실패할 수 있습니다.
운영자는 reschedule
스탠자를 사용하여 어떤 상황에서 실패한 task를 다른 노드로 일정을 변경해야 하는지 Nomad에게 알려줍니다.
Nomad는 이전에 해당 task에 사용되지 않은 노드로 일정을 변경하는 것을 선호합니다. restart
절과 마찬가지로 Nomad가 시도해야 하는 재스케줄 시도(attempts) 횟수, Nomad가 지연된 재스케줄 시도 사이에 대기해야 하는 시간(delay)과 간격(interval)으로 시도된 재스케줄 시도를 제한하는 시간을 지정할 수 있습니다.
또한 delay_function
을 사용하여 초기 지연(delay) 후 후속 일정 변경 시도를 계산하는 데 사용할 함수를 지정합니다. 옵션은 constant
, exponential
, fibonacci
입니다. 서비스 Job의 경우 피보나치(fibonacci) 스케줄링은 초기에 짧은 수명 중단에서 복구하기 위해 빠르게 일정을 재조정하는 반면 더 긴 중단 동안 이탈을 피하기 위해 속도를 늦추는 좋은 속성이 있습니다. exponential
와 fibonacci
지연 함수를 사용할 때 max_delay
를 사용하여 지연 시간의 상한선을 설정하고 그 이후에는 증가하지 않습니다. 무제한 재스케줄 시도를 활성화하거나 활성화하지 않으려면 unlimited
를 true
또는 false
로 설정합니다.
다시 스케줄링(reschedule)하는 동작을 완전히 비활성화하려면 attempts = 0
및 unlimited = false
로 설정합니다.
다음 reschedule
스탠자는 Nomad에게 task group 일정을 무제한으로 다시 예약하고 후속 시도 사이의 지연을 기하급수적으로 늘리도록 지시합니다. 시작 지연은 30초에서 최대 1시간입니다.
group "cache" {
+ ...
+ reschedule {
+ delay = "30s"
+ delay_function = "exponential"
+ max_delay = "1hr"
+ unlimited = true
+ }
+}
+
재스케줄 스탠자는 모든 노드에서 실행되기 때문에 시스템 작업에 적용되지 않습니다.
Nomad 버전 0.8.4부터는 배포 중에 reschedule
스탠자가 적용됩니다.
기존 데이터 센터에서 노드 오류는 모니터링 시스템에 의해 감지되고 운영자에게 경고를 트리거합니다. 그런 다음 운영자는 장애가 발생한 노드를 복구하거나 워크로드를 다른 노드로 마이그레이션하기 위해 수동으로 개입해야 합니다. reschedule
기능을 통해 운영자는 가장 일반적인 실패 시나리오에 대한 계획을 세울 수 있으며 Nomad는 수동 개입의 필요성을 피하면서 자동으로 응답합니다. Nomad는 합리적인 기본값을 적용하므로 대부분의 사용자는 다양한 재시작 매개변수에 대해 생각할 필요 없이 로컬 재시작 및 일정 변경을 얻을 수 있습니다.
운영자는 restart
절을 사용하여 실패한 작업에 대한 Nomad의 동일 노드에 대한 로컬 재시작 전략을 지정합니다. Consul 및 check_restart
스탠자와 함께 사용하면 Nomad는 restart
매개변수에 따라 응답하지 않는 작업을 동일 노드에서 다시 시작합니다. 운영자는 reschedule
절을 사용하여 실패한 Job을 다른 노드에 다시 예약하기 위한 Nomad의 전략을 지정합니다.
docker 런타임에는 log driver로 "awslogs"를 지원합니다.
https://docs.docker.com/config/containers/logging/awslogs/
팁
Nomad에서 docker 자체의 로깅을 사용하므로서, Nomad에서 실행되는 docker 기반 컨테이너의 로깅이 특정 환경에 락인되는것을 방지합니다.
경고
AWS 환경이 아닌 외부 구성 시, 해당 노드에 Cloudwath 기록을 위한 Policy를 갖는 IAM의 credential 정보가 환경변수 또는 ~/.aws/credential
구성이 필요합니다.
Nomad 구성 시 Cloudwatch에 대한 EC2 Instance의 IAM 구성이 필요합니다. 아래 Terraform 구성의 예를 참고하세요.
aws_iam_role_policy
에 설정하는 필요한 권한에 차이가 있을 수 있습니다.awslogs-create-group = true
옵션을 추가하려는 경우 logs:CreateLogGroup
정책이 필요합니다.## 생략 ##
+
+resource "aws_iam_instance_profile" "ec2_profile" {
+ name = "ec2_profile"
+ role = aws_iam_role.role.name
+}
+
+resource "aws_iam_role" "role" {
+ name = "my_role"
+
+ assume_role_policy = jsonencode({
+ Version = "2012-10-17"
+ Statement = [
+ {
+ Action = "sts:AssumeRole"
+ Effect = "Allow"
+ Sid = ""
+ Principal = {
+ Service = "ec2.amazonaws.com"
+ }
+ },
+ ]
+ })
+}
+
+resource "aws_iam_role_policy" "cloudwatch_policy" {
+ name = "cloudwatch_policy"
+ role = aws_iam_role.role.id
+
+ policy = jsonencode({
+ Version = "2012-10-17"
+ Statement = [
+ {
+ Action = [
+ "logs:CreateLogStream",
+ "logs:PutLogEvents",
+ "logs:CreateLogGroup"
+ ]
+ Effect = "Allow"
+ Resource = "*"
+ },
+ ]
+ })
+}
+
+resource "aws_instance" "example" {
+ ami = "ami-04e6fcf8cfe3b09ea"
+ instance_type = "t2.micro"
+ key_name = aws_key_pair.web_admin.key_name
+ vpc_security_group_ids = [
+ aws_security_group.ssh.id
+ ]
+
+ iam_instance_profile = aws_iam_instance_profile.ec2_profile.name
+}
+
Nomad에서 docker 드라이버 사용시 적용되는 기본 log driver는 json-file
입니다. 추가 설정을 통해 docker가 지원하는 다양한 log driver를 사용할 수 있습니다. (FluentD 샘플)
--log-driver
같은 옵션의 정의가 HCL형태로 정의됩니다."\[%Y-%m-%d\]"
에서 [
같은 특수문자 표기를 위해 \
를 한번만 넣었다면, "\\[%Y-%m-%d\\]"
같이 두번 넣어야 할수도 있습니다.구성 예제는 아래와 같습니다.
job "api" {
+ datacenters = ["dc1"]
+ type = "service"
+
+ group "api" {
+ network {
+ mode = "bridge"
+ port "api" {
+ to = 9001
+ }
+ }
+
+ service {
+ name = "count-api"
+ port = "api"
+ connect {
+ sidecar_service {}
+ }
+ }
+
+ task "web" {
+ driver = "docker"
+ config {
+ image = "hashicorpnomad/counter-api:v1"
+ ports = ["api"]
+ logging {
+ type = "awslogs"
+ config {
+ awslogs-region = "ap-northeast-2"
+ awslogs-group = "myGroup"
+ awslogs-create-group = true
+ awslogs-datetime-format = "\\[%Y-%m-%dT%H:%M:%S\\+09:00\\]"
+ }
+ }
+ }
+ }
+ }
+}
+
Nomad의 로그 출력을 확인합니다.
Cloudwatch에 로그 출력을 확인합니다.
Nomad Version : >= 1.0.0
Nomad Ent. Version : >= 0.7.0
https://learn.hashicorp.com/tutorials/nomad/namespaces
$ nomad namespace apply -description "PoC Application" apps
+
$ nomad namespace delete apps
+
$ nomad namespace list
+Name Description
+default Default shared namespace
+
job "rails-www" {
+
+ ## Run in the QA environments
+ namespace = "web-qa"
+
+ ## Only run in one datacenter when QAing
+ datacenters = ["us-west1"]
+ # ...
+}
+
# flag 설정
+nomad job status -namespace=web-qa
+
+# ENV 설정
+export NOMAD_NAMESPACE=web-qa
+nomad job status
+
# Allow read only access to the production namespace
+namespace "web-prod" {
+ policy = "read"
+}
+
+# Allow writing to the QA namespace
+namespace "web-qa" {
+ policy = "write"
+}
+
팁
해당 Token의 policy는 특정인이 원하여 만들었으며, 더 다양한 제약과 허용을 할 수 있습니다. 해당 policy는 아래와 같은 제약과 허용을 합니다.
#원하는 권한이 있는 policy file
+$ cat nomad-ui-policy.hcl
+namespace "*" {
+ policy = "read"
+ capabilities = ["submit-job", "dispatch-job", "read-logs", "list-jobs", "parse-job", "read-job", "csi-list-volume", "csi-read-volume", "list-scaling-policies", "read-scaling-policy", "read-job-scaling", "read-fs"]
+}
+node {
+ policy = "read"
+}
+host_volume "*" {
+ policy = "write"
+}
+plugin {
+ policy = "read"
+}
+
+#위에서 만든 policy 파일을 nomad cluster에 적용
+$ nomad acl policy apply -description "Production UI policy" prod-ui nomad-ui-policy.hcl
+
+#해당 policy로 token생성(policy는 여러개를 넣을 수도 있음)
+$ nomad acl token create -name="prod ui token" -policy=prod-ui -type=client | tee ui-prod.token
+#웹 브라우저 로그인을 위해 Secret ID 복사
+
아래는 위에서 만들어진 토큰으로 로그인한 화면입니다.
아래 그림과 같이 exec버튼이 비활성화되어 있는 걸 볼 수 있습니다.
팁
공식 사이트에 consul 인증서 생성 가이드는 있는데 Nomad 인증서 생성가이드는
Show Terminal을 들어가야 볼 수 있기때문에 귀찮음을 해결하기 위해 공유합니다.
consul tls ca create -domain=nomad -days 3650
+
+consul tls cert create -domain=nomad -dc=global -server -days 3650
+
+consul tls cert create -domain=nomad -dc=global -client -days 3650
+
+consul tls cert create -domain=nomad -dc=global -cli -days 3650
+
export NOMAD_CACERT="${HOME}/tls/nomad-agent-ca.pem"
+
+export NOMAD_CLIENT_CERT="${HOME}/tls/global-cli-nomad-0.pem"
+
+export NOMAD_CLIENT_KEY="${HOME}/tls/global-cli-nomad-0-key.pem"
+
+export NOMAD_ADDR="https://127.0.0.1:4646"
+
팁
최대한 설정값을 넣어보고, 번역기도 돌려보고 물어도 보고 넣은 server설정 파일입니다.
네트워크는 프라이빗(온프레이머스) 환경입니다.
#nomad server 설정
+server {
+ enabled = true
+ bootstrap_expect = 3
+ license_path="/opt/nomad/license/nomad.license"
+ server_join {
+ retry_join = ["172.30.1.17","172.30.1.18","172.30.1.19"]
+ }
+ raft_protocol = 3
+ event_buffer_size = 100
+ non_voting_server = false
+ heartbeat_grace = "10s"
+}
+
+
+#tls 설정
+tls {
+ http = true
+ rpc = true
+
+ ca_file = "/opt/ssl/nomad/nomad-agent-ca.pem"
+ cert_file = "/opt/ssl/nomad/global-server-nomad-0.pem"
+ key_file = "/opt/ssl/nomad/global-server-nomad-0-key.pem"
+
+ #UI오픈할 서버만 변경
+ verify_server_hostname = false
+ verify_https_client = false
+ #일반서버는 아래와 같이 설정
+ verify_server_hostname = true
+ verify_https_client = true
+}
+
data_dir = "/opt/consul"
+
+client_addr = "0.0.0.0"
+
+datacenter = "my-dc"
+
+#ui
+ui_config {
+ enabled = true
+}
+
+# server
+server = true
+
+# Bind addr
+bind_addr = "0.0.0.0" # Listen on all IPv4
+# Advertise addr - if you want to point clients to a different address than bind or LB.
+advertise_addr = "node ip"
+
+# Enterprise License
+license_path = "/opt/nomad/nomad.lic"
+
+# bootstrap_expect
+bootstrap_expect=1
+
+# encrypt
+encrypt = "7w+zkhqa+YD4GSKXjRWETBIT8hs53Sr/w95oiVxq5Qc="
+
+# retry_join
+retry_join = ["server ip"]
+
+key_file = "/opt/consul/my-dc-server-consul-0-key.pem"
+cert_file = "/opt/consul/my-dc-server-consul-0.pem"
+ca_file = "/opt/consul/consul-agent-ca.pem"
+auto_encrypt {
+ allow_tls = true
+}
+
+verify_incoming = false
+verify_incoming_rpc = false
+verify_outgoing = false
+verify_server_hostname = false
+
+ports {
+ http = 8500
+ dns = 8600
+ server = 8300
+}
+
+
팁
최대한 설정값을 넣어보고, 번역기도 돌려보고 물어도 보고 넣은 Client설정 파일입니다.
네트워크는 프라이빗(온프레이머스) 환경입니다.
#nomad client 설정
+
+client {
+ enabled = true
+ servers = ["172.30.1.17","172.30.1.18","172.30.1.19"]
+ server_join {
+ retry_join = ["172.30.1.17","172.30.1.18","172.30.1.19"]
+ retry_max = 3
+ retry_interval = "15s"
+ }
+ #host에서 nomad에서 사용할 수 있는 volume 설정
+ host_volume "logs" {
+ path = "/var/logs/elk/"
+ read_only = false
+ }
+ #각각의 client의 레이블 작성
+ #meta {
+ # name = "moon"
+ # zone = "web"
+ #}
+ #nomad에서 예약할 자원
+ reserved {
+ #Specifies the amount of CPU to reserve, in MHz.
+ cpu = 200
+ #Specifies the amount of memory to reserve, in MB.
+ memory = 8192
+ #Specifies the amount of disk to reserve, in MB.
+ disk = 102400
+ }
+ no_host_uuid = true
+ #bridge network interface name
+ bridge_network_name = "nomad"
+ bridge_network_subnet = "172.26.64.0/20"
+ cni_path = "/opt/cni/bin"
+ cni_config_dir = "/opt/cni/config"
+}
+#tls 설정
+tls {
+ http = true
+ rpc = true
+
+ ca_file = "/opt/ssl/nomad/nomad-agent-ca.pem"
+ cert_file = "/opt/ssl/nomad/global-client-nomad-0.pem"
+ key_file = "/opt/ssl/nomad/global-client-nomad-0-key.pem"
+
+ verify_server_hostname = true
+ verify_https_client = true
+}
+
data_dir = "/opt/nomad/data"
+bind_addr = "0.0.0.0"
+
+client {
+ enabled = true
+ servers = ["server ip"]
+ # sidecar image 고정
+ meta {
+ connect.sidecar_image = "envoyproxy/envoy:v1.21.3"
+ }
+}
+
+#consul 정보 입력
+consul {
+ address = "127.0.0.1:8501"
+ grpc_address="127.0.0.1:8502"
+ server_service_name = "nomad"
+ client_service_name = "nomad-client"
+ auto_advertise = true
+ server_auto_join = true
+ client_auto_join = true
+ ssl = true
+ verify_ssl = false
+ ca_file = "/opt/consul/consul-agent-ca.pem"
+ cert_file = "/opt/consul/my-dc-client-consul-0.pem"
+ key_file = "/opt/consul/my-dc-client-consul-0-key.pem"
+}
+
+plugin "docker" {
+ config {
+ auth {
+ }
+ }
+}
+
+
팁
최대한 설정값을 넣어보고, 번역기도 돌려보고 물어도 보고 넣은 server, client의 공통설정 파일입니다.
저는 agent.hcl파일안에 다 넣고 실행하지만 나눠서 추후에는 기능별로 나눠서 사용할 예정입니다.
#nomad 공통 설정
+datacenter = "dc1"
+region = "global"
+data_dir = "/opt/nomad/nomad"
+bind_addr = "{{ GetInterfaceIP `ens192` }}"
+
+advertise {
+ # Defaults to the first private IP address.
+ #http = "{{ GetInterfaceIP `ens244` }}"
+ #rpc = "{{ GetInterfaceIP `ens244` }}"
+ #serf = "{{ GetInterfaceIP `ens244` }}"
+ http = "{{ GetInterfaceIP `ens192` }}"
+ rpc = "{{ GetInterfaceIP `ens192` }}"
+ serf = "{{ GetInterfaceIP `ens192` }}"
+}
+
+consul {
+ address = "127.0.0.1:8500"
+ server_service_name = "nomad"
+ client_service_name = "nomad-client"
+ auto_advertise = true
+ server_auto_join = true
+ client_auto_join = true
+ #consul join용 token
+ token = "33ee4276-e1ef-8e5b-d212-1f94ca8cf81e"
+}
+enable_syslog = false
+enable_debug = false
+disable_update_check = false
+
+log_level = "DEBUG"
+log_file = "/var/log/nomad/nomad.log"
+log_rotate_duration = "24h"
+log_rotate_bytes = 104857600
+log_rotate_max_files = 100
+
+ports {
+ http = 4646
+ rpc = 4647
+ serf = 4648
+}
+
+#prometheus에서 nomad의 metrics값을 수집 해 갈 수 있게 해주는 설정
+telemetry {
+ collection_interval = "1s"
+ disable_hostname = true
+ prometheus_metrics = true
+ publish_allocation_metrics = true
+ publish_node_metrics = true
+}
+
+
+plugin "docker" {
+ config {
+ auth {
+ config = "/root/.docker/config.json"
+ }
+ #온프레이머스환경에서는 해당 이미지를 private repository에 ㅓㄶ고 변경
+ infra_image = "google-containers/pause-amd64:3.1"
+ }
+}
+
+acl {
+ enabled = true
+}
+
Consul Enterprise는 Namespace
가 있어서 Nomad로 Consul에 서비스 등록 시 특정 Namespace를 지정할 수 있음
Job > Group > Consul
job "frontback_job" {
+ group "backend_group_v1" {
+
+ count = 1
+
+ consul {
+ namespace = "mynamespace"
+ }
+
+ service {
+ name = "backend"
+ port = "http"
+
+ connect {
+ sidecar_service {}
+ }
+
+ check {
+ type = "http"
+ path = "/"
+ interval = "5s"
+ timeout = "3s"
+ }
+ }
+# 생략
+
해당 group
에 대한 글로벌 설정이기 때문에 Consul과 관련해서 구성되는 모든 설정은 해당 Namespace
를 기준으로 적용됨
예를 들어 upstream
구성을 하면
job "frontback_job" {
+ group "frontend_group" {
+ count = 1
+
+ consul {
+ namespace = "mesh"
+ }
+
+ service {
+ name = "frontend"
+ port = "http"
+
+ connect {
+ sidecar_service {
+ proxy {
+ upstreams {
+ destination_name = "backend"
+ local_bind_port = 10000
+ }
+ }
+ }
+ }
+# 생략
+
sidecar의 로그에서도 적용된 namespace로 리스너가 등록되는 로그(namesapce/servicename
) 확인 가능
[2021-09-01 01:31:10.490][1][info][upstream] [source/common/upstream/cds_api_helper.cc:28] cds: add 3 cluster(s), remove 0 cluster(s)
+[2021-09-01 01:31:10.572][1][info][upstream] [source/common/upstream/cds_api_helper.cc:65] cds: added/updated 3 cluster(s), skipped 0 unmodified cluster(s)
+[2021-09-01 01:31:10.572][1][info][upstream] [source/common/upstream/cluster_manager_impl.cc:168] cm init: initializing secondary clusters
+[2021-09-01 01:31:10.574][1][info][upstream] [source/common/upstream/cluster_manager_impl.cc:192] cm init: all clusters initialized
+[2021-09-01 01:31:10.574][1][info][main] [source/server/server.cc:745] all clusters initialized. initializing init manager
+[2021-09-01 01:31:10.578][1][info][upstream] [source/server/lds_api.cc:78] lds: add/update listener 'mesh/backend:127.0.0.1:10000'
+[2021-09-01 01:31:10.586][1][info][upstream] [source/server/lds_api.cc:78] lds: add/update listener 'public_listener:0.0.0.0:24945'
+[2021-09-01 01:31:10.587][1][info][config] [source/server/listener_manager_impl.cc:888] all dependencies initialized. starting workers
+[2021-09-01 01:46:10.592][1][info][main] [source/server/drain_manager_impl.cc:70] shutting down parent after drain
+
경고
주의할점은 DNS를 사용하는 경우, 예를들어 template 작성시 namespace가 추가되면 경로상 datacenter도 정의해줘야 인식하는 것으로 보임
[tag.]<service>.service.<namespace>.<datacenter>.<domain>
+
참고 링크 : https://www.consul.io/docs/discovery/dns#namespaced-services
기존 템플릿
template {
+ data = <<EOF
+defaults
+ mode http
+
+frontend http_front
+ bind *:28888
+ default_backend http_back
+
+backend http_back
+ balance roundrobin
+ server-template mywebapp 2 _frontend._tcp.service.consul resolvers consul resolve-opts allow-dup-ip resolve-prefer ipv4 check
+
+resolvers consul
+ nameserver consul 127.0.0.1:8600
+ accepted_payload_size 8192
+ hold valid 5s
+EOF
+
+ destination = "local/haproxy.cfg"
+}
+
Namespace 적용 템플릿
template {
+ data = <<EOF
+defaults
+ mode http
+
+frontend http_front
+ bind *:28888
+ default_backend http_back
+
+backend http_back
+ balance roundrobin
+ server-template mywebapp 2 _frontend._tcp.service.mesh.hashistack.consul resolvers consul resolve-opts allow-dup-ip resolve-prefer ipv4 check
+
+resolvers consul
+ nameserver consul 127.0.0.1:8600
+ accepted_payload_size 8192
+ hold valid 5s
+EOF
+
+ destination = "local/haproxy.cfg"
+}
+
# nomad namespace apply -description "ServiceMesh Sample" mesh
+
+locals {
+ mode = "Legacy"
+ namespace = "mesh"
+ #artifact = "https://hashicorpjp.s3.ap-northeast-1.amazonaws.com/masa/Snapshots2021Jan_Nomad/frontback.tgz"
+ artifact = "https://github.com/Great-Stone/Snapshots_2021Jan_Pseudo-containerized/raw/main/frontback.tgz"
+ node = "https://github.com/Great-Stone/Snapshots_2021Jan_Pseudo-containerized/raw/main/nodejs-linux"
+ subject = "snapshot"
+}
+
+variables {
+ frontend_port = 8080
+ upstream_port = 10000
+}
+
+variable "attrib_v1" {
+ type = object({
+ version = string
+ task_count = number
+ text_color = string
+ })
+ default = {
+ version = "v1"
+ task_count = 1
+ text_color = "green"
+ }
+}
+
+variable "attrib_v2" {
+ type = object({
+ version = string
+ task_count = number
+ text_color = string
+ })
+ default = {
+ version = "v2"
+ task_count = 1
+ text_color = "red"
+ }
+}
+
+job "frontback_job" {
+
+ region = "global"
+ datacenters = ["hashistack"]
+ namespace = local.namespace
+
+ type = "service"
+
+ constraint {
+ attribute = "${meta.subject}"
+ value = local.subject
+ }
+
+ #######################
+ # #
+ # Backend v1 #
+ # #
+ #######################
+
+ group "backend_group_v1" {
+
+ count = var.attrib_v1["task_count"]
+
+ consul {
+ namespace = local.namespace
+ }
+
+ network {
+ mode = "bridge"
+ port "http" {}
+ }
+
+ service {
+ name = "backend"
+ port = "http"
+
+ connect {
+ sidecar_service {}
+ }
+
+ meta {
+ version = var.attrib_v1["version"]
+ }
+
+ check {
+ type = "http"
+ path = "/"
+ interval = "5s"
+ timeout = "3s"
+ }
+
+ tags = [
+ "Snapshots",
+ "Backend",
+ local.mode,
+ var.attrib_v1["version"]
+ ]
+ }
+
+ task "backend" {
+
+ driver = "exec"
+
+ artifact {
+ source = local.artifact
+ }
+
+ env {
+ COLOR = var.attrib_v1["text_color"]
+ MODE = local.mode
+ TASK_ID = NOMAD_ALLOC_INDEX
+ ADDR = NOMAD_ADDR_http
+ PORT = NOMAD_PORT_http
+ VERSION = var.attrib_v1["version"]
+ # IMG_SRC = "${local.img_dir}${var.attrib_v1["version"]}.png"
+ }
+
+ config {
+ command = "backend"
+ }
+
+ resources {
+ memory = 32 # reserve 32 MB
+ cpu = 100 # reserve 100 MHz
+ }
+
+ }
+
+ reschedule {
+ delay = "10s"
+ delay_function = "constant"
+ }
+ }
+
+ #######################
+ # #
+ # Backend v2 #
+ # #
+ #######################
+
+ group "backend_group_v2" {
+
+ count = var.attrib_v2["task_count"]
+
+ consul {
+ namespace = local.namespace
+ }
+
+ network {
+ mode = "bridge"
+ port "http" {}
+ }
+
+ service {
+ name = "backend"
+ port = "http"
+
+ connect {
+ sidecar_service {}
+ }
+
+ meta {
+ version = var.attrib_v2["version"]
+ }
+
+ check {
+ type = "http"
+ path = "/"
+ interval = "5s"
+ timeout = "3s"
+ }
+
+ tags = [
+ "Snapshots",
+ "Backend",
+ local.mode,
+ var.attrib_v2["version"]
+ ]
+ }
+
+ task "backend" {
+
+ driver = "exec"
+
+ artifact {
+ source = local.artifact
+ }
+
+ env {
+ COLOR = var.attrib_v2["text_color"]
+ MODE = local.mode
+ TASK_ID = NOMAD_ALLOC_INDEX
+ ADDR = NOMAD_ADDR_http
+ PORT = NOMAD_PORT_http
+ VERSION = var.attrib_v2["version"]
+ # IMG_SRC = "${local.img_dir}${var.attrib_v2["version"]}.png"
+ }
+
+ config {
+ command = "backend"
+ }
+
+ resources {
+ memory = 32 # reserve 32 MB
+ cpu = 100 # reserve 100 MHz
+ }
+ }
+
+ reschedule {
+ delay = "10s"
+ delay_function = "constant"
+ }
+ }
+
+ ######################
+ # #
+ # Frontend #
+ # #
+ ######################
+
+ group "frontend_group" {
+
+ count = 1
+
+ consul {
+ namespace = local.namespace
+ }
+
+ network {
+ mode = "bridge"
+ port "http" {
+ // static = var.frontend_port
+ }
+ }
+
+ service {
+ name = "frontend"
+ port = "http"
+
+ connect {
+ sidecar_service {
+ proxy {
+ upstreams {
+ destination_name = "backend"
+ local_bind_port = var.upstream_port
+ }
+ }
+ }
+ }
+
+ // check {
+ // type = "http"
+ // path = "/"
+ // interval = "5s"
+ // timeout = "3s"
+ // }
+
+ tags = [
+ local.mode,
+ "Snapshots",
+ "Frontend"
+ ]
+ }
+
+ task "frontend" {
+
+ driver = "exec"
+
+ artifact {
+ source = local.node
+ }
+
+ env {
+ PORT = NOMAD_PORT_http
+ UPSTREAM_URL = "http://${NOMAD_UPSTREAM_ADDR_backend}"
+ }
+
+ config {
+ command = "nodejs-linux"
+ }
+
+ resources {
+ memory = 32 # reserve 32 MB
+ cpu = 100 # reserve 100 MHz
+ }
+
+ }
+
+ reschedule {
+ delay = "10s"
+ delay_function = "constant"
+ }
+ }
+
+ ######################
+ # #
+ # haproxy #
+ # #
+ ######################
+
+ group "haproxy" {
+ count = 1
+
+ consul {
+ namespace = local.namespace
+ }
+
+ network {
+ port "http" {
+ static = 28888
+ }
+
+ port "stats" {
+ static = 21936
+ }
+ }
+
+ task "haproxy" {
+ driver = "docker"
+
+ config {
+ image = "haproxy:2.0"
+ network_mode = "host"
+
+ volumes = [
+ "local/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg",
+ ]
+
+ ports = ["http", "stats"]
+ }
+
+ template {
+ data = <<EOF
+defaults
+ mode http
+
+frontend stats
+ bind *:21936
+ stats uri /
+ stats show-legends
+ no log
+
+frontend http_front
+ bind *:28888
+ default_backend http_back
+
+backend http_back
+ balance roundrobin
+ server-template mywebapp 2 _frontend._tcp.service.mesh.hashistack.consul resolvers consul resolve-opts allow-dup-ip resolve-prefer ipv4 check
+
+resolvers consul
+ nameserver consul 127.0.0.1:8600
+ accepted_payload_size 8192
+ hold valid 5s
+EOF
+
+ destination = "local/haproxy.cfg"
+ }
+
+ service {
+ name = "haproxy"
+
+ check {
+ name = "alive"
+ type = "tcp"
+ port = "http"
+ interval = "10s"
+ timeout = "2s"
+ }
+ }
+
+ resources {
+ cpu = 200
+ memory = 128
+
+ network {
+ mbits = 10
+
+ // port "http" {
+ // static = 28888
+ // to = 8888
+ // }
+
+ // port "stats" {
+ // static = 21936
+ // to = 1936
+ // }
+ }
+ }
+ }
+ }
+}
+
job "plugin-nfs-controller" {
+ datacenters = ["dc1"]
+
+ group "controller" {
+ task "plugin" {
+ driver = "docker"
+
+ config {
+ image = "mcr.microsoft.com/k8s/csi/nfs-csi:latest"
+
+ args = [
+ "--endpoint=unix://csi/csi.sock",
+ "--nodeid=${attr.unique.hostname}",
+ "--logtostderr",
+ "-v=5",
+ ]
+ }
+
+ csi_plugin {
+ id = "nfs"
+ type = "controller"
+ mount_dir = "/csi"
+ }
+
+ resources {
+ cpu = 250
+ memory = 128
+ }
+ }
+ }
+}
+
+
job "plugin-nfs-nodes" {
+ datacenters = ["dc1"]
+
+ type = "system"
+
+ group "nodes" {
+ task "plugin" {
+ driver = "docker"
+
+ config {
+ image = "mcr.microsoft.com/k8s/csi/nfs-csi:latest"
+
+ args = [
+ "--endpoint=unix://csi/csi.sock",
+ "--nodeid=${attr.unique.hostname}",
+ "--logtostderr",
+ "--v=5",
+ ]
+
+ privileged = true
+ }
+
+ csi_plugin {
+ id = "nfs"
+ type = "node"
+ mount_dir = "/csi"
+ }
+
+ resources {
+ cpu = 250
+ memory = 128
+ }
+ }
+ }
+}
+
+
id = "nfs-vol"
+name = "nfs"
+type = "csi"
+external_id = "nfs"
+plugin_id = "nfs"
+#snapshot_id = "test" # or clone_id, see below
+capacity_max = "20G"
+capacity_min = "10G"
+
+capability {
+ access_mode = "single-node-writer"
+ attachment_mode = "file-system"
+}
+
+mount_options {
+ fs_type = "ext4"
+ mount_flags = ["noatime"]
+}
+
+context {
+ server = "10.0.0.151"
+ share = "/mnt/data"
+}
+
volume "nfs-vol" {
+ type = "csi"
+ source = "nfs-vol"
+ read_only = false
+ attachment_mode = "file-system"
+ access_mode = "single-node-writer"
+ }
+
Nomad ui 설정에 다음과 같이 Consul과 Vault의 링크를 추가할 수 있습니다.
ui {
+ enabled = true
+
+ consul {
+ ui_url = "https://consul.example.com:8500/ui"
+ }
+
+ vault {
+ ui_url = "https://vault.example.com:8200/ui"
+ }
+}
+
# 사용된 policy들
+$ cat nomad-cluster-role.json
+{
+ "allowed_policies": "admin",
+ "token_explicit_max_ttl": 0,
+ "name": "nomad-cluster",
+ "orphan": true,
+ "token_period": 259200,
+ "renewable": true
+}
+vault write /auth/token/roles/nomad-cluster @nomad-cluster-role.json
+
+$ cat admin-policy.hcl
+# Read system health check
+path "sys/health"
+{
+ capabilities = ["read", "sudo"]
+}
+
+# Create and manage ACL policies broadly across Vault
+
+# List existing policies
+path "sys/policies/acl"
+{
+ capabilities = ["list"]
+}
+
+# Create and manage ACL policies
+path "sys/policies/acl/*"
+{
+ capabilities = ["create", "read", "update", "delete", "list", "sudo"]
+}
+
+# Enable and manage authentication methods broadly across Vault
+
+# Manage auth methods broadly across Vault
+path "auth/*"
+{
+ capabilities = ["create", "read", "update", "delete", "list", "sudo"]
+}
+
+# Create, update, and delete auth methods
+path "sys/auth/*"
+{
+ capabilities = ["create", "update", "delete", "sudo"]
+}
+
+# List auth methods
+path "sys/auth"
+{
+ capabilities = ["read"]
+}
+
+# Enable and manage the key/value secrets engine at `secret/` path
+
+# List, create, update, and delete key/value secrets
+path "secret/*"
+{
+ capabilities = ["create", "read", "update", "delete", "list", "sudo"]
+}
+
+# Manage secrets engines
+path "sys/mounts/*"
+{
+ capabilities = ["create", "read", "update", "delete", "list", "sudo"]
+}
+
+# List existing secrets engines.
+path "sys/mounts"
+{
+ capabilities = ["read"]
+}
+
+vault policy write admin admin-policy.hcl
+
+# token 생성
+vault token create -policy admin -period 72h -orphan
+
vault {
+ enabled = true
+ address = "http://active.vault.service.consul:8200"
+ task_token_ttl = "1h"
+ create_from_role = "nomad-cluster"
+ token = "s.hQRpxLmyk6AgSKJWOc9Gmbj1"
+}
+
vault {
+ enabled = true
+ address = "http://active.vault.service.consul:8200"
+}
+
└── hashicorp
+ └── nomad
+ ├── config
+ └── data
+
$PATH
위치에 복사 하거나 등록 </hashicorp/nomad/config/nomad.hcl>
datacenter = "dc1"
+data_dir = "/hashicorp/nomad/data"
+
+bind_addr = "0.0.0.0"
+
+advertise {
+ http = "{{ GetInterfaceIP \"eth1\" }}"
+ rpc = "{{ GetInterfaceIP \"eth1\" }}"
+ serf = "{{ GetInterfaceIP \"eth1\" }}"
+}
+
+server {
+ enabled = true
+ bootstrap_expect = 1
+}
+
{{ GetInterfaceIP \"eth1\" }}
+
retry_join
에 Server의 주소 꼭 넣기!
</hashicorp/nomad/config/nomad.hcl>
datacenter = "dc1"
+data_dir = "/hashicorp/nomad/data"
+
+bind_addr = "0.0.0.0"
+
+server {
+ enabled = false
+}
+
+server_join {
+ retry_join = ["<server_ip>:4647"]
+}
+
+client {
+ enabled = true
+ servers = ["<server_ip>:4647"]
+ meta {
+ "key1" = "value1"
+ "key2" = "value2"
+ }
+ options = {
+ "driver.raw_exec.enable" = "1"
+ }
+}
+
https://learn.hashicorp.com/tutorials/nomad/production-deployment-guide-vm-with-consul
sudo touch /etc/systemd/system/nomad.service
+
ExecStart
의 nomad 바이너리 경로에 주의!!!ExecStart
의 -config
에 앞서 작성한 config 파일 디렉토리 경로 맞추기!!![Unit]
+Description=Nomad
+Documentation=https://www.nomadproject.io/docs/
+Wants=network-online.target
+After=network-online.target
+
+[Service]
+
+# Nomad server should be run as the nomad user. Nomad clients
+# should be run as root
+User=nomad
+Group=nomad
+
+ExecReload=/bin/kill -HUP $MAINPID
+ExecStart=/usr/local/bin/nomad agent -config /etc/nomad.d
+KillMode=process
+KillSignal=SIGINT
+LimitNOFILE=65536
+LimitNPROC=infinity
+Restart=on-failure
+RestartSec=2
+
+## Configure unit start rate limiting. Units which are started more than
+## *burst* times within an *interval* time span are not permitted to start any
+## more. Use `StartLimitIntervalSec` or `StartLimitInterval` (depending on
+## systemd version) to configure the checking interval and `StartLimitBurst`
+## to configure how many starts per interval are allowed. The values in the
+## commented lines are defaults.
+
+# StartLimitBurst = 5
+
+## StartLimitIntervalSec is used for systemd versions >= 230
+# StartLimitIntervalSec = 10s
+
+## StartLimitInterval is used for systemd versions < 230
+# StartLimitInterval = 10s
+
+TasksMax=infinity
+OOMScoreAdjust=-1000
+
+[Install]
+WantedBy=multi-user.target
+
$ systemctl start nomad
+$ systemctl enable nomad
+
Job 실행은 CLI, API, UI 실행 가능
$ NOMAD_ADDR=http://<server_ip>:4646
+$ nomad job run <job_file_path>
+
http://<server_ip>:4646
에 접속WORKLOAD/Jobs
선택Run Job
버튼 클릭Plan
, Apply
job "batch" {
+ datacenters = ["dc1"]
+
+ type = "batch"
+
+ group "batch-1" {
+ count = 1
+ task "batch" {
+ driver = "raw_exec"
+ template {
+ data = <<EOF
+#!/bin/bash
+echo $(hostname) > /tmp/check.txt
+EOF
+ destination = "run.sh"
+ }
+ config {
+ command = "run.sh"
+ }
+ resources {
+ cpu = 100
+ memory = 64
+ }
+ }
+
+ task "batch-2" {
+ driver = "raw_exec"
+ artifact {
+ source = "http://<shared_ip>:<port>/path/run.sh"
+ destination = "local"
+ }
+ config {
+ command = "local/run.sh"
+ }
+ resources {
+ cpu = 100
+ memory = 64
+ }
+ }
+ }
+}
+
+
driver
가 raw_exec
이면 isolation 없이 스크립트를 실행하는 방식template
에서 작성하는 파일은 동적으로 생성되며 변수 조합도 가능artifact
를 정의하여 중앙 저장소의 파일을 다운로드 받아 구성 가능job "periodic" {
+ datacenters = ["dc1"]
+
+ type = "batch"
+
+ periodic {
+ cron = "*/5 * * * * * *"
+ prohibit_overlap = true
+ time_zone = "Asia/Seoul"
+ }
+
+ constraint {
+ attribute = "${attr.unique.hostname}"
+ value = "cn-client-2"
+ }
+
+ group "batch" {
+ count = 1
+ task "batch" {
+ driver = "raw_exec"
+ template {
+ data = <<EOF
+#!/bin/bash
+echo $(date) >> /tmp/periodic.txt
+EOF
+ destination = "run.sh"
+ }
+ config {
+ command = "run.sh"
+ }
+ resources {
+ cpu = 100
+ memory = 64
+ }
+ }
+ }
+}
+
periodic
에서 cron 형태로 정의 가능
constraint
은 조건을 부여하는 옵션으로 attribute와 meta 정보를 활용 가능, 해당 예제에서는 hostname 기준으로 동작 타겟 호스트를 정의함
attribute를 조건으로 주는 방법은 attribute값 앞에 attr.
prefix를 추가
meta의 경우 meta로 선언된 키 앞에 meta.
prefix를 추가
CLI로 확인하는 방법
$ nomad agent-info | grep node_id
+ node_id = ae3cf7ee-09e6-c158-d883-fe4e4f39eb2b
+$ nomad node status -verbose ae3cf7ee-09e6-c158-d883-fe4e4f39eb2b
+ID = ae3cf7ee-09e6-c158-d883-fe4e4f39eb2b
+Name = gs-C02CT3ZFML85
+...
+Attributes
+cpu.arch = amd64
+cpu.frequency = 2300
+cpu.modelname = Intel(R) Core(TM) i7-1068NG7 CPU @ 2.30GHz
+cpu.numcores = 8
+cpu.totalcompute = 18400
+driver.java = 1
+driver.java.runtime = OpenJDK Runtime Environment Temurin-11.0.14.1+1 (build 11.0.14.1+1)
+driver.java.version = 11.0.14.1
+...
+Meta
+connect.gateway_image = envoyproxy/envoy:v${NOMAD_envoy_version}
+connect.log_level = info
+connect.proxy_concurrency = 1
+...
+
UI로 확인하는 방법
http://<server_ip>:4646
에 접속CLUSTER/Clients
선택job "param" {
+ datacenters = ["dc1"]
+
+ type = "batch"
+
+ parameterized {
+ payload = "optional"
+ meta_required = ["param"]
+ }
+
+ constraint {
+ attribute = "${attr.unique.hostname}"
+ value = "cn-client-1"
+ }
+
+ group "batch" {
+ count = 1
+ task "batch" {
+ driver = "raw_exec"
+ template {
+ data = <<EOF
+#!/bin/bash
+echo 'batch param {{ env "NOMAD_META_PARAM" }}' >> /tmp/param.txt
+EOF
+ destination = "run.sh"
+ }
+ config {
+ command = "run.sh"
+ }
+ resources {
+ cpu = 100
+ memory = 64
+ }
+ }
+ }
+}
+
parameterized
항목에서 json형태의 payload, 또는 URL Param 형태를 입력 받아 동작 가능job "install_docker" {
+ datacenters = ["dc1"]
+
+ type = "sysbatch"
+
+ // periodic {
+ // cron = "*/5 * * * * * *"
+ // prohibit_overlap = true
+ // time_zone = "Asia/Seoul"
+ // }
+
+ constraint {
+ attribute = "${attr.os.name}"
+ value = "ubuntu"
+ }
+
+ group "install" {
+ count = 1
+ task "docker" {
+ driver = "raw_exec"
+ template {
+ data = <<EOF
+#!/bin/bash
+apt-get update
+apt-get install -y apt-transport-https ca-certificates curl software-properties-common
+curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
+add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable"
+apt-get update
+apt-cache policy docker-ce
+apt-get install docker-ce -y
+systemctl enable docker
+systemctl start docker
+EOF
+ destination = "docker_install.sh"
+ }
+ config {
+ command = "docker_install.sh"
+ }
+ resources {
+ cpu = 100
+ memory = 64
+ }
+ }
+ }
+}
+
+
job "system" {
+ datacenters = ["dc1"]
+
+ type = "system"
+
+ group "cache" {
+ count = 1
+
+ network {
+ port "db" {
+ to = 6379
+ }
+ }
+
+ task "redis" {
+ driver = "docker"
+
+ config {
+ image = "redis:6.2.6-alpine3.15"
+ ports = ["db"]
+ }
+
+ resources {
+ cpu = 500
+ memory = 512
+ }
+ }
+ }
+}
+
redis:6.2.6-alpine3.15
를 모든 노드에서 실행하도록 구성group/network
에서 사용할 network 조건을 정의 job "service" {
+ datacenters = ["dc1"]
+
+ // spread {
+ // attribute = "${node.datacenter}"
+ // }
+
+ group "nginx" {
+ count = 2
+
+ scaling {
+ enabled = true
+ min = 0
+ max = 3
+ }
+
+
+ network {
+ port "http" {
+ to = 80
+ static = 18080
+ }
+ }
+
+ service {
+ name = "nginx-backend"
+ port = "http"
+ tags = ["prod"]
+ }
+
+ task "nginx" {
+ driver = "docker"
+
+ config {
+ image = "nginx"
+ ports = ["http"]
+ volumes = [
+ "local/html:/usr/share/nginx/html",
+ ]
+ }
+
+ template {
+ data = <<EOF
+ <h1>Welcome to {{ env "NOMAD_JOB_NAME" }} Production {{ env "NOMAD_HOST_PORT_http" }}</h1>
+ node_dc: {{ env "node.datacenter" }}<br>
+ node_hostname: {{ env "attr.unique.hostname" }}<br>
+ node_cores: {{ env "attr.cpu.numcores" }}<br>
+ os_name: {{ env "attr.os.name" }}<br>
+ cpu_model: {{ env "attr.cpu.modelname" }}<br>
+ EOF
+ destination = "local/html/index.html"
+ }
+ }
+ }
+}
+
count
가 2 이므로 해당 서비스는 2개를 띄우려고 시도scaling
정의가 있는 경우 UI/CLI/API 에서 scaling count값 지정 가능static
명시가 되어있으므로 해당 서비스는 18080
을 사용할 수 있는 count 만큼의 노드가 필요Nomad를 Windows환경에 구성하고 실행을위해 서비스로 등록하는 방법을 알아봅니다. 솔루션 실행 환경 또는 운영/개발자의 익숙함 정도에 따라 다양한 OS를 선택하여 애플리케이션을 배포하게 됩니다. Nomad를 통해 배포를 위한 오케스트레이터를 Windows 환경에 적용하고 서비스에 등록하여 상시적으로 실행될 수 있도록하는 구성을 안내합니다.
참고 url : Port used
Nomad는 서버와 클라이언트 모드로 나뉩니다. 서버를 위해서는 3 개의 포트가 필요하고 클라이언트에서는 2 개의 포트가 필요합니다. 클라이언트에 배포되는 애플리케이션에서 사용하는 포트를 동적으로 할당하는 영역을 구성합니다.
종류 | 기본값 | 프로토콜 | 설명 |
---|---|---|---|
HTTP API | 4646 | TCP | 서버와 클라이언트에서 HTTP API를 제공하는데 사용됩니다. |
RPC | 4647 | TCP | 서버와 클라이언트간의 내부. RPC 통신 및 서버간 통신에 사용됩니다. |
Serf WAN | 4648 | TCP/UDP | 서버간 LAN/WAN 으로 GOSSIP 프로토콜로 사용됩니다. |
Dynamic | 1025–60000 | TCP/UDP | 클라이언트에서 사용할 동적 포트를 할당합니다. |
Windows에서의 동적포트 설명은 다음을 참고합니다. : Ephemeral_port
Windows에서의 동적포트 설정은 다음을 참고합니다. : default-dynamic-port-range-tcpip-chang
디렉토리 구성의 예는 아래와 같습니다.
└── Nomad
+ ├── bin
+ ├── config
+ └── data
+
PATH
로 등록 하거나 C:\WINDOWS\system32\
의 경로에 바이너리를 위치시키는 방법도 있습니다.설치 참고 url : Install
Dev모드 참고 url : Get Start
Windows에 설치하는 방식은 수동, Chocolatey, 컴파일 방식을 지원합니다. 여기서는 수동 구성 방법에 대해 설명합니다.
미리 컴파일된 바이너리 파일은 다음의 경로에서 확인할 수 있습니다. 2021년 4월 18일 기준 1.0.4 버전이 최신 버전입니다.
nomad_<버전>
으로 표기됩니다.nomad_<버전>+ent
로 표기됩니다.릴리즈 된 zip을 다운로드 받고 적절한 위치에 압축을 해제 합니다. 위 디렉토리 구성 에서의 예로는 bin
디렉토리 아래 위치 시킵니다.
Nomad 버전을 확인하여 바이너리가 정상적으로 실행되는지 확인합니다.
PS C:₩hashicorp₩nomad₩bin> ./nomad.exe version
+Nomad v1.0.4 (9294f35f9aa8dbb4acb6e85fa88e3e2534a3e41a)
+
Dev 모드로 실행하여 테스트 하는것도 가능합니다.
PS C:₩hashicorp₩nomad₩bin> ./nomad agent -dev
+==> No configuration files loaded
+==> Starting Nomad agent...
+==> Nomad agent configuration:
+
+ Advertise Addrs: HTTP: 127.0.0.1:4646; RPC: 127.0.0.1:4647; Serf: 127.0.0.1:4648
+ Bind Addrs: HTTP: 127.0.0.1:4646; RPC: 127.0.0.1:4647; Serf: 127.0.0.1:4648
+ Client: true
+ Log Level: DEBUG
+ Region: global (DC: dc1)
+ Server: true
+ Version: 1.0.4
+
+==> Nomad agent started! Log data will stream in below:
+...생략...
+
실행 후 나열된 정보를 사용하여 UI에 접속해봅니다.
설정 설명 url : Configuration
go_sockaddr_template : go-sockaddr
Nomad 실행시 CLI 상에 설정을 하는 Inline 방식과 설정파일을 지정하는 방식으로 구성이 가능합니다. 여기서는 구성파일을 지정하도록 하는 방식을 설명합니다.
설정 파일을 디렉토리 구성에서 지정한 config
디렉토리에 위치 시킵니다. 여기서는 예로 nomad.hcl
이라 명명합니다. 테스트를 위한 몇가지 설정요소를 아래에 설명합니다.
[nomad.hcl]
datacenter = "dc1"
+data_dir = "C:\\hashicorp\\nomad\\data"
+bind_addr = "0.0.0.0"
+
+advertise {
+ // http = "{{ GetInterfaceIP \"Network 1\" }}"
+ rpc = "{{ GetInterfaceIP \"Network 1\" }}"
+ serf = "{{ GetInterfaceIP \"Network 1\" }}"
+}
+
+server {
+ enabled = true
+ bootstrap_expect = 1
+}
+
+client {
+ enabled = true
+ network_interface = "Network 2"
+ meta {
+ subject = "server1"
+ purpose = "test,sample"
+ }
+ options = {
+ driver.raw_exec.enable = "1"
+ }
+}
+
+server_join {
+ retry_join = ["{{ GetInterfaceIP \"Network 1\" }}:4647"]
+}
+
datacenter
로 클러스터의 클라이언트들을 그룹화 할 수 있습니다. Nomad에 배포하는 대상으로 지정됩니다. 기본값은 dc1
입니다.data
디렉토리를 지정하였습니다. \\
를 사용합니다.0.0.0.0
으로 설정하였습니다.bind_addr
값을 상속 받지만, 사용자가 지정한 네트워크 주소를 지정하기 위해 사용됩니다. ip 형식을 사용할 수도 있고 go-sockaddr 템플릿 구성을 사용 가능하기 때문에 템플릿 형태로 지정도 가능합니다. 예제에서는 네트워크 인터페이스에 할당된 IP를 가져오는 방식 입니다. 한글도 지원합니다. {{ GetInterfaceIP \"이더넷 1\" }}
+
true
인 경우 서버 모드로 실행됩니다.true
인 경우 클라이언트 모드로 실행됩니다.구성파일을 작성하였다면, 해당 구성파일을 지정하여 Nomad를 실행할 수 있습니다.
PS C:₩hashicorp₩nomad₩bin> ./nomad agent -config=C:\hashicorp\nomad\config\nomad.hcl
+
Windows 서비스로 등록하는 경우 다음을 참고합니다.
sc.exe delete nomad
+sc.exe create nomad binPath= "C:\hashicorp\nomad\bin\nomad.exe agent -config=C:\hashicorp\nomad\config\nomad.hcl" start= auto
+net start nomad
+
Job 구성 url : Manage-job
Windows에서만 실행되는 커맨드를 활용하여 동작을 테스트 합니다.
[test.nomad]
job "test" {
+ datacenters = ["dc1"]
+ type = "batch"
+
+ constraint {
+ attribute = "${attr.kernel.name}"
+ value = "windows"
+ }
+
+ group "windows" {
+ count = 1
+ task "systeminfo" {
+ driver = "raw_exec"
+ config {
+ command = "C:\\windows\\system32\\systeminfo"
+ }
+ }
+ }
+}
+
driver
의 실행을위한 구성을 정의합니다. 예제에서는 raw_exec
를 사용하였으므로 command
를 입력합니다.job의 내용은 파일로 구성하여 CLI로 등록하는 것도 가능하고 UI에서 입력하는 것도 가능합니다. UI등록의 예는 다음과 같습니다.
UI의 좌측 Jobs
를 클릭하여 우측 상단의 Run Job
버튼을 클릭합니다.
Job Definition
란에 job 정의를 채우고 Plan
을 클릭합니다.
Plan의 결과를 확인하고 Run
버튼으로 배포를 실행합니다.
배포의 결과를 확인합니다.
Task Groups
의 task 이름을 클릭하면 원인을 확인할 수 있습니다.Running
상태로 확인됩니다. batch
의 경우 실행 후 종료되기 때문에 최종적으로 Complete
상태가 됩니다.Allocation
항목을 클릭하면 마지막 실행 task를 확인할 수 있고, 원격에서 실행된 Log
나 생성된 File
을 확인할 수 있습니다.Windows 환경에 실행되는 애플리케이션을 원격에서 일괄적으로 관리하기위한 환경을 제공합니다. CI/CD 과정에서 마지막 단계인 배포 동작에 대해 API를 지원하고 스케쥴링 및 배포의 상태를 관리해주는 역할로 동작합니다. Batch, Service, System 의 배포 실행 형태를 지정할 수 있고, 다양한 실행 드라이버(exec, java, docker, 등)을 지원하여 다중의 OS 환경 및 온프레미스와 클라우드 환경 전반에 배포를 위한 쉽고 간단한 워크로드 오케스트레이션 환경을 구성할 수 있습니다.
$ sw_vers
+ProductName: macOS
+ProductVersion: 12.4
+
+$ brew --version
+Homebrew 3.5.2
+
+$ git version
+git version 2.27.0
+
+$ java -version
+openjdk version "11.0.14.1" 2022-02-08
+
+$ gradle --version
+Welcome to Gradle 7.4.2!
+
+$ docker version
+Client:
+ Version: 20.10.9
+
+Server:
+ Engine:
+ Version: 20.10.14
+
+$ vault version
+Vault v1.11.0
+
+$ nomad version
+Nomad v1.3.1
+
+$ curl --version
+curl 7.79.1 (x86_64-apple-darwin21.0)
+
+$ aws --version
+aws-cli/2.7.11 Python/3.10.5 Darwin/21.5.0 source/x86_64 prompt/off
+
vault server -dev -dev-root-token-id=root
+
Another terminal
export VAULT_ADDR=http://127.0.0.1:8200
+export VAULT_TOKEN=root
+export NOMAD_POLICY=nomad-server
+
cat <<EOF | vault policy write $NOMAD_POLICY -
+# Allow creating tokens under "nomad-cluster" token role. The token role name
+# should be updated if "nomad-cluster" is not used.
+path "auth/token/create/nomad-cluster" {
+ capabilities = ["update"]
+}
+
+# Allow looking up "nomad-cluster" token role. The token role name should be
+# updated if "nomad-cluster" is not used.
+path "auth/token/roles/nomad-cluster" {
+ capabilities = ["read"]
+}
+
+# Allow looking up the token passed to Nomad to validate # the token has the
+# proper capabilities. This is provided by the "default" policy.
+path "auth/token/lookup-self" {
+ capabilities = ["read"]
+}
+
+# Allow looking up incoming tokens to validate they have permissions to access
+# the tokens they are requesting. This is only required if
+# `allow_unauthenticated` is set to false.
+path "auth/token/lookup" {
+ capabilities = ["update"]
+}
+
+# Allow revoking tokens that should no longer exist. This allows revoking
+# tokens for dead tasks.
+path "auth/token/revoke-accessor" {
+ capabilities = ["update"]
+}
+
+# Allow checking the capabilities of our own token. This is used to validate the
+# token upon startup.
+path "sys/capabilities-self" {
+ capabilities = ["update"]
+}
+
+# Allow our own token to be renewed.
+path "auth/token/renew-self" {
+ capabilities = ["update"]
+}
+EOF
+
cat <<EOF | vault policy write aws_policy -
+path "aws/creds/s3" {
+ capabilities = ["read","update"]
+}
+EOF
+
vault write auth/token/roles/nomad-cluster allowed_policies="aws_policy,db_policy" disallowed_policies="$NOMAD_POLICY" token_explicit_max_ttl=0 orphan=true token_period="259200" renewable=true
+
vault token create -field token -policy $NOMAD_POLICY -period 72h -orphan > /tmp/token.txt
+
nomad agent -dev -vault-enabled=true -vault-address=http://127.0.0.1:8200 -vault-token=$(cat /tmp/token.txt) -vault-tls-skip-verify=true -vault-create-from-role=nomad-cluster -alloc-dir=/tmp/nomad/alloc -state-dir=/tmp/nomad/state
+
Another terminal
export NOMAD_ADDR=http://127.0.0.1:4646
+
cat <<EOF | nomad job run -
+job "fileserver" {
+ datacenters = ["dc1"]
+
+ group "fileserver" {
+ count = 1
+
+ network {
+ port "http" {
+ to = 3000
+ static = 3000
+ }
+ }
+
+ task "fileserver" {
+ driver = "docker"
+
+ config {
+ image = "julienmeerschart/simple-file-upload-download-server"
+ ports = ["http"]
+ }
+ }
+ }
+}
+EOF
+
Upload Test
$ curl -F file=@/tmp/dynamic.properties http://localhost:3000
+{"downloadLink":"http://localhost:3000/file?file=dynamic.properties","curl":"curl http://localhost:3000/file?file=dynamic.properties > dynamic.properties"}
+
$ nomad agent-info
+client
+ heartbeat_ttl = 11.955357358s
+ known_servers = 127.0.0.1:4647
+ last_heartbeat = 9.248352347s
+ node_id = 69944736-5399-f805-9c03-35be83c9abfe
+ num_allocations = 0
+nomad
+ bootstrap = true
+ known_regions = 1
+ leader = true
+ leader_addr = 127.0.0.1:4647
+ server = true
+<...>
+vault
+ token_expire_time = 2022-06-30T08:44:26+09:00
+ token_last_renewal_time = 2022-06-27T08:44:26+09:00
+ token_next_renewal_time = 2022-06-28T20:44:26+09:00
+ token_ttl = 71h53m46s
+ tracked_for_revoked = 0
+
AWS Dynamic Secret
export AWS_ACCESS_KEY=AKIAU3NXXXXX
+export AWS_SECRET_KEY=Rex3GPUKO3++123
+export AWS_REGION=ap-northeast-2
+
vault secrets enable aws
+
vault write aws/config/root \
+ access_key=$AWS_ACCESS_KEY \
+ secret_key=$AWS_SECRET_KEY \
+ region=$AWS_REGION
+
vault write /aws/config/lease lease=1m lease_max=1m
+
vault write aws/roles/s3 \
+ credential_type=iam_user \
+ policy_document=-<<EOF
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:PutObject",
+ "s3:PutObjectAcl"
+ ],
+ "Resource": "*"
+ }
+ ]
+}
+EOF
+
$ vault read aws/creds/s3
+Key Value
+--- -----
+lease_id aws/creds/s3/tt1sONqTebOsrJxBs6A3B4m4
+lease_duration 1m
+lease_renewable true
+access_key AKIAU3NXDWRUL5GVEB4H
+secret_key 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
+security_token <nil>
+
macOS guide : https://www.jenkins.io/download/lts/macos/
brew install jenkins-lts
+
brew services start jenkins-lts
+
Home 디렉토리의 Jenkins 활성화를 위한 패스워드를 다음 경로에서 복사하여 http://localhost:8080 페이지의 Unlock Jenkins
에 입력
빠른 시작을 위해 기본 값인 Install suggested plugins
를 클릭
계정명, 암호, 이름, 이메일 주소를 기입하고 Save and Continue
버튼 클릭
올바른 Jenkins URL을 확인하고 Save and Finish
버튼 클릭
Start using Jenkins
버튼 클릭
GitHub 로그인
우측 상단 사용자 메뉴 클릭 후 Settings
클릭
좌측 메뉴 최하단 Developer settings
클릭
좌측 메뉴 Personal access tokens
클릭
Generate new token
버튼 클릭
Token 옵션 선택 후 Generate token
클릭
생성된 토큰을 기록/보관
Jenkins 관리
> 시스템 설정
으로 이동
JDK
항목에서 Add JDK
클릭
Git
항목에서 Add Git
을 클릭
GitHub
항목에서 Add GitHub Server
드롭박스의 GitHub Server
를 클릭
+Add
버튼 클릭하여 Jenkins
선택 후 새로운 크리덴셜 생성 후 생성된 항목 지정Secret Test
선택Test Connection
버튼으로 연결 확인Gradle
항목에서 Add Gradle
클릭
Name : 이름 입력 (e.g. gradle)
GRADLE_HOME : Gradle 홈 디렉토리 입력 (e.g. /usr/local/Cellar/gradle/7.4.2/libexec)
demo>src>main>resources>application.yml
dynamic:
+ path: ${DYNAMIC_PROPERTIES_PATH:/tmp/dynamic.properties}
+server:
+ port: ${NOMAD_HOST_PORT_http:8080}
+
demo>src>main>java>com>example>demo>DemoApplication.java
package com.example.demo;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.List;
+
+@RestController
+@SpringBootApplication
+@EnableScheduling
+public class DemoApplication {
+
+ private static String FILE_PATH;
+
+ @Value("${dynamic.path}")
+ public void setKey(String value) {
+ FILE_PATH = value;
+ }
+
+ public static void main(String[] args) {
+ SpringApplication.run(DemoApplication.class, args);
+ }
+
+ @Scheduled(fixedRate=1000)
+ public void filecheck() throws IOException {
+ List<String> str = Files.readAllLines(Paths.get(FILE_PATH));
+ System.out.println(str);
+ }
+
+ @RequestMapping(method = RequestMethod.GET, path = "/")
+ public String index() throws IOException {
+ List<String> str = Files.readAllLines(Paths.get(FILE_PATH));
+ System.out.println(str);
+
+ return "<h1>AWS</h1>"
+ .concat("<h2>" + str.get(0) + "</h2>")
+ .concat("<h2>" + str.get(1) + "</h2>");
+ }
+}
+
cat <<EOF> /tmp/dynamic.properties
+aws_access_key=my_access_key
+aws_secret_key=my_secret_key
+EOF
+
$ gradle bootRun
+...
+[aws_access_key=my_access_key, aws_secret_key=my_secret_key]
+[aws_access_key=my_access_key, aws_secret_key=my_secret_key]
+<==========---> 80% EXECUTING [5s]
+> :bootRun
+
Pipeline 구성 case 1
- GitHub checkout
- Gradle build
- jar upload
- Nomad Job Start
Pipeline 구성 case 2
- GitHub checkout
- Gradle build
- docker build
- docker push
- Nomad Job Start
좌측 + 새로운 Item
버튼 클릭
이름 입력 (e.g. Nomad Job - Java Driver)
Pipeline 선택 후 OK
성성된 Jenkins Job의 Pipeline에 스크립트 구성
pipeline {
+ agent any
+ triggers {
+ cron('H */8 * * *') //regular builds
+ pollSCM('* * * * *') //polling for changes, here once a minute
+ }
+ tools {
+ git('local')
+ gradle('gradle')
+ jdk("jdk11")
+ }
+ environment {
+ NOMAD_ADDR = 'http://localhost:4646'
+ }
+ stages {
+ stage('Clone') {
+ steps {
+ git branch: 'main',
+ credentialsId: 'jenkins_github',
+ url: 'https://github.com/Great-Stone/jenkins-gradle-nomad-pipeline'
+ sh "ls -lat"
+ }
+ }
+ stage('Test') {
+ steps {
+ sh './gradlew test'
+ }
+ }
+ stage('Build') {
+ steps {
+ sh './gradlew build'
+ }
+ }
+ stage('Upload') {
+ steps {
+ sh 'mv ./build/libs/demo-0.0.1-SNAPSHOT.jar ./demo-${BUILD_NUMBER}.jar'
+ sh 'curl -F file=@./demo-${BUILD_NUMBER}.jar http://localhost:3000'
+ }
+ }
+ stage('Nomad Download') {
+ steps {
+ sh 'curl -C - --output nomad_1.3.1_darwin_amd64.zip https://releases.hashicorp.com/nomad/1.3.1/nomad_1.3.1_darwin_amd64.zip'
+ sh 'unzip -o nomad_1.3.1_darwin_amd64.zip'
+ }
+ }
+ stage('Deploy To Nomad') {
+ input{
+ message "Do you want to proceed for production deployment?"
+ }
+ steps {
+ sh './nomad job run -var version=${BUILD_NUMBER} ./nomad-java.hcl'
+ }
+ }
+ }
+}
+
지금 빌드
를 클릭하여 빌드를 진행합니다. 마지막 단계에서 마우스 오버하여 승인처리합니다.
ECR - AWS 구성 필요
리포지토리 이름을 입력 후 태그 변경 불가능 옵션과 푸시할 때 스캔 설정
리포지토리 생성 후 URI 복사
e.g. <id>.dkr.ecr.ap-northeast-2.amazonaws.com/demo
Jenkins 관리
> 플러그인 관리
로 이동
설치 가능
탭을 선택하고 AWS Global Configuration
, Docker
, Docker Pipeline
, Amazon ECR
를 검색 후 설치
Jenkins 관리
> Manage Credentials
로 이동
Domains의 (global) 항목 선택하여 Add credentials
클릭
Kind : Secret text
Scope : Global
ID : 이름 (e.g. ecr_cred)
Secret: JSON 형태의 Secret
aws_access_key_id
aws_secret_access_key
aws_session_token (sts 인 경우)
{"aws_access_key_id":"ASIA2LEU5EPEJQGXNMJU","aws_secret_access_key":"psGEnC5COCwcUojDo6EO/Ztd7J58THSVerEc7EE9","aws_session_token":"IQoJb3JpZ2luX2VjEMj//////////wEaCXVzLXdlc3QtMiJIMEYCIQDkdZ+GEya0j8gxM/Ow5GD5Kjr8e//pA/hARZm2Tok+JgIhAOrl0c8ctXkerxsNgSwAKSjmIrdbUyHxXVsuPl+GHgywKuAECOH//////////wEQARoMNzExMTI5Mzc1Njg4IgxAc1B5sczNlB7TgyAqtASCHs37YorM6spNTNftpvajNFGewy4z8ztDo83qx5+I67ldNnWnJBt2IHCYdtLBp1/wd/8yGYFKb/TWuOgjo0pdDKc2wJQ7gAbnB8d65OYzP2SSipqUJ/E4Tz/Ojgb0UQiAd8GcXEMdEe+9WBciSK5AD2CraMmbYlq2ThBjot8BOXJHG688IbI29/Qq+Y1WpozDpjNaeLm+kd9a9GWtX1XUXUvaXDcz+81RKW271uUp4n0JhfJ5PPUilxQVfISXtv7rEpp1PjhDE3c/oK6muK8SeeIIX7fAGi1cXqHMw9PfolQY6oAHUp3Acq+8uakNyOw8Usfl9xRDf1hQyAfofsz00DCmiZinuam8dg32R1pHW7JRBYgXXD5/dnp1A7KgdrjhjECpU8+Ayo/dAOqohOfTBYS/xMrVs8tkfUxHd/AKCca5oYda1YyIxdweBp/kEZHCZTkEpzY2TxEtzVsm5fEbjTViemglVmnkeDoZGeERVyXJlxVzW2of8I3hmKAeCENcH5L+Y"}
+
좌측 + 새로운 Item
버튼 클릭
이름 입력 (e.g. Nomad Job - Docker Driver)
Pipeline 선택 후 OK
성성된 Jenkins Job의 Pipeline에 스크립트 구성 (Private)
pipeline {
+ agent any
+ triggers {
+ cron('H */8 * * *') //regular builds
+ pollSCM('* * * * *') //polling for changes, here once a minute
+ }
+ tools {
+ git('local')
+ gradle('gradle')
+ jdk("jdk11")
+ }
+ environment {
+ NOMAD_ADDR = 'http://localhost:4646'
+ NOMAD_DOWNLOAD_URL = 'https://releases.hashicorp.com/nomad/1.3.1/nomad_1.3.1_darwin_amd64.zip'
+ DOCKER_REGISTRY = '**********.dkr.ecr.ap-northeast-2.amazonaws.com'
+ dockerImage = ''
+ PATH = "/Users/gs/.rd/bin:${PATH}"
+ }
+ stages {
+ stage('Clone') {
+ steps {
+ git branch: 'main',
+ credentialsId: 'jenkins_github',
+ url: 'https://github.com/Great-Stone/jenkins-gradle-nomad-pipeline'
+ sh "ls -lat"
+ }
+ }
+ stage('Test') {
+ steps {
+ sh './gradlew test'
+ }
+ }
+ stage('Java Build') {
+ steps {
+ sh './gradlew build'
+ }
+ }
+ stage('Docker Build') {
+ steps {
+ script{
+ dockerImage = docker.build DOCKER_REGISTRY + "/demo:${BUILD_NUMBER}"
+ }
+ }
+ }
+ stage('Docker Push') {
+ steps {
+ script {
+ withCredentials([string(credentialsId: 'sts', variable: 'CREDS')]) {
+ def creds = readJSON text: CREDS
+ withEnv([
+ "AWS_ACCESS_KEY_ID=${creds.aws_access_key_id}",
+ "AWS_SECRET_ACCESS_KEY=${creds.aws_secret_access_key}",
+ "AWS_SESSION_TOKEN=${creds.aws_session_token}"
+ ]) {
+ sh '''
+ ECR_TOKEN=$(/usr/local/bin/aws ecr get-login-password --region ap-northeast-2)
+ echo "${ECR_TOKEN}" > ecr_token.txt
+
+ echo "${ECR_TOKEN}" | docker login -u AWS --password-stdin ${DOCKER_REGISTRY}
+ '''
+ dockerImage.push()
+ }
+ }
+ }
+ }
+ }
+ stage('Nomad Download') {
+ steps {
+ sh 'curl -C - --output nomad.zip ${NOMAD_DOWNLOAD_URL}'
+ sh 'unzip -o nomad.zip'
+ }
+ }
+ stage('Deploy To Nomad') {
+ input{
+ message "Do you want to proceed for production deployment?"
+ }
+ steps {
+ sh './nomad job run -var image=${DOCKER_REGISTRY}/demo -var tag=${BUILD_NUMBER} -var ecr_token=$(cat ecr_token.txt) ./nomad-docker.hcl'
+ }
+ }
+ }
+ post {
+ always {
+ sh 'docker rmi ${DOCKER_REGISTRY}/demo:${BUILD_NUMBER}'
+ }
+ }
+
+}
+
지금 빌드
를 클릭하여 빌드를 진행합니다. 마지막 단계에서 마우스 오버하여 승인처리합니다.
HCL로 작성된 Job의 경우 Nomad CLI 또는 UI 접속이 가능하다면 바로 적용 가능하다.
job "2048-game" {
+ datacenters = ["dc1"]
+ type = "service"
+ group "game" {
+ count = 1 # number of instances
+
+ network {
+ port "http" {
+ static = 80
+ }
+ }
+
+ task "2048" {
+ driver = "docker"
+
+ config {
+ image = "alexwhen/docker-2048"
+
+ ports = [
+ "http"
+ ]
+
+ }
+
+ resources {
+ cpu = 500 # 500 MHz
+ memory = 256 # 256MB
+ }
+ }
+ }
+}
+
nomad job run 2048.hcl
+
하지만 CLI/UI를 사용할 수 없는 환경에서 API를 사용하여 Job을 실행해야하는 경우, 특히 CICD Pipeline구성에서 API를 사용하여 Job을 실행해야하는 경우 HCL을 Json 형식으로 변경해야하는 경우가 있다.
HCL을 Json으로 변경하는 방식의 첫번째는 CLI를 사용하는 방식이다.
nomad job run -output 2048.hcl > payload.json
+
하지만 이 경우 -output
을 입력하지 않는 경우 Job이 실행되는 실수의 여지가 있고, CLI가 없다면 사용 불가하다.
다음은 API를 사용하는 방식이다.
Parsh Job : https://developer.hashicorp.com/nomad/api-docs/jobs#parse-job
문서의 내용처럼 HCL을 한줄로 변경하여 API로 요청하면 Json으로 형태를 출력해준다.
{
+ "JobHCL": "job \"example\" {\n type = \"service\"\n group \"cache\" {}\n}",
+ "Canonicalize": true
+}
+
HCL을 한줄로 변경하기 까다롭거나 별도의 도구가 없다면 jq
를 활용한 방식도 가이드하고 있다.
jq -Rsc '{ JobHCL: ., Canonicalize: true }' example.nomad.hcl > payload.json
+
/v1/jobs/parse
엔드포인트로 payload.json
데이터를 담아 요청한다.
curl \
+ --request POST \
+ --data @payload.json \
+ https://localhost:4646/v1/jobs/parse
+
Json으로 변경된 값을 반환한다.
{
+ "AllAtOnce": false,
+ "Constraints": null,
+ "Affinities": null,
+ "CreateIndex": 0,
+ "Datacenters": null,
+ "ID": "my-job",
+
API가 제공하는 Json Parse를 사용, 다음과 같은 순서로 Job을 실행할 수 있다.
hcl.json
으로 생성jq -Rsc '{ JobHCL: ., Canonicalize: true }' 2048.hcl > hcl.json
+
/v1/jobs/parse
엔드포인트로 요청하여 Json형태로 파싱curl --request POST --data @hcl.json http://127.0.0.1:4646/v1/jobs/parse
+
한가지 문제는, Job의 Json 정의에는 Job
이라는 키값이 최상위에 존재해야하는데, 반환되는 결과에는 Job
하위부터 출력된다. 따라서 jq
를 사용하여 다음과 같이 출력을 수정하여 저장한다.
curl --request POST --data @hcl.json http://127.0.0.1:4646/v1/jobs/parse | jq -s '{ Job: .[] }' > 2048.json
+
curl --request POST --data @2048.json http://127.0.0.1:4646/v1/jobs
+
위 과정을 다음과 같이 한줄로 정의할 수 있다.
jq -Rsc '{ JobHCL: ., Canonicalize: true }' 2048.hcl | \
+curl --request POST --data @- http://127.0.0.1:4646/v1/jobs/parse | \
+jq -s '{ Job: .[] }' - | \
+curl --request POST --data @- http://127.0.0.1:4646/v1/jobs
+
GitHub 리소스 : https://github.com/Great-Stone/nomad-springboot-graceful-shutdown
application.yml
구성server:
+ port: 8080
+ shutdown: graceful
+spring:
+ lifecycle:
+ timeout-per-shutdown-phase: 35s # Default 30s
+
server.shutdown
에 graceful
정의 필요spring.lifecycle.timeout-per-shutdown-phase
에 Graceful Shutdown 요청시 지연 시간 설정테스트는 빌드(gradle build
) 후 해당 jar파일에 대해 실행
실행 커맨드 예시 : java -jar ./build/libs/demo-0.0.1-SNAPSHOT.jar
테스트 API : http://localhost:8080/test/1
$ curl http://localhost:8080/test/1
+(코드에서 지정한 20000ms 지연)
+Process Success !!
+
Graceful Shutdown 종료를 위해 kill -15 <PID>
형태의 시그널 전달 필요
-9
)대신 TERM(-15
)를 사용SIGTERM 사용시 종료 메시지 확인 및 정상 응답 확인
. ____ _ __ _ _
+/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
+( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
+\\/ ___)| |_)| | | | | || (_| | ) ) ) )
+ ' |____| .__|_| |_|_| |_\__, | / / / /
+=========|_|==============|___/=/_/_/_/
+:: Spring Boot :: (v2.7.7)
+
+2023-01-12 14:10:14.572 INFO 45334 --- [ main] com.example.demo.DemoApplication : Starting DemoApplication using Java 11.0.14.1 on gs-C02CT3ZFML85 with PID 45334 (/private/var/folders/5r/8y6t82xd1h183tq1l_whv8yw0000gn/T/NomadClient2000479524/d5d8f4a6-4fd7-87ea-b40b-93f0227371db/boot/local/uc started by gs in /private/var/folders/5r/8y6t82xd1h183tq1l_whv8yw0000gn/T/NomadClient2000479524/d5d8f4a6-4fd7-87ea-b40b-93f0227371db/boot)
+2023-01-12 14:10:14.575 INFO 45334 --- [ main] com.example.demo.DemoApplication : No active profile set, falling back to 1 default profile: "default"
+2023-01-12 14:10:15.462 INFO 45334 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
+2023-01-12 14:10:15.475 INFO 45334 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
+2023-01-12 14:10:15.476 INFO 45334 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.70]
+2023-01-12 14:10:15.553 INFO 45334 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
+2023-01-12 14:10:15.553 INFO 45334 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 922 ms
+2023-01-12 14:10:15.948 INFO 45334 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
+2023-01-12 14:10:15.957 INFO 45334 --- [ main] com.example.demo.DemoApplication : Started DemoApplication in 1.876 seconds (JVM running for 2.331)
+2023-01-12 14:10:26.982 INFO 45334 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
+2023-01-12 14:10:26.982 INFO 45334 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
+2023-01-12 14:10:26.983 INFO 45334 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms
+2023-01-12 14:10:27.022 INFO 45334 --- [nio-8080-exec-1] com.example.demo.TestController : ========================== Start Process -> Process Number: 4
+2023-01-12 14:10:32.243 INFO 45334 --- [ionShutdownHook] o.s.b.w.e.tomcat.GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complete
+2023-01-12 14:10:47.030 INFO 45334 --- [nio-8080-exec-1] com.example.demo.TestController : ========================== End Process -> Process Number: 4
+2023-01-12 14:10:47.089 INFO 45334 --- [tomcat-shutdown] o.s.b.w.e.tomcat.GracefulShutdown : Graceful shutdown complete
+
Commencing graceful shutdown. Waiting for active requests to complete
메시지 출력Graceful shutdown complete
메시지 출력job "graceful" {
+ datacenters = ["dc1"]
+
+ group "java" {
+ task "boot" {
+ driver = "java"
+
+ kill_timeout = "40s"
+ kill_signal = "SIGTERM"
+
+ config {
+ jar_path = "local/demo-0.0.1-SNAPSHOT.jar"
+ jvm_options = ["-Xmx2048m", "-Xms256m"]
+ }
+
+ artifact {
+ source = "https://github.com/Great-Stone/nomad-springboot-graceful-shutdown/releases/download/0.0.1/demo-0.0.1-SNAPSHOT.jar"
+ }
+ }
+ }
+}
+
SIGKILL
이므로 task 정의에서 kill_signal
을 SIGTERM
으로 변경 필요kill_timeout
기본 값이 5초 이므로, Graceful Shutdown을 적용하려는 애플리케이션의 정의 보다 크게 설정 필요팁
테스트 및 사용전 확인해야 할 사항은 Nomad의 enterprise, 즉 라이선스가 필요하며, nomad-autosclaer의 경우에도 enterprise여야만 합니다.
job "example" {
+ datacenters = ["dc1"]
+
+ group "cache-lb" {
+ count = 1
+
+ network {
+ port "lb" {}
+ }
+
+ task "nginx" {
+ driver = "docker"
+
+ config {
+ image = "nginx"
+ ports = ["lb"]
+ volumes = [
+ # It's safe to mount this path as a file because it won't re-render.
+ "local/nginx.conf:/etc/nginx/nginx.conf",
+ # This path hosts files that will re-render with Consul Template.
+ "local/nginx:/etc/nginx/conf.d"
+ ]
+ }
+
+ # This template overwrites the embedded nginx.conf file so it loads
+ # conf.d/*.conf files outside of the `http` block.
+ template {
+ data = <<EOF
+user nginx;
+worker_processes 1;
+error_log /var/log/nginx/error.log warn;
+pid /var/run/nginx.pid;
+events {
+ worker_connections 1024;
+}
+include /etc/nginx/conf.d/*.conf;
+EOF
+ destination = "local/nginx.conf"
+ }
+
+ # This template creates a TCP proxy to Redis.
+ template {
+ data = <<EOF
+stream {
+ server {
+ listen {{ env "NOMAD_PORT_lb" }};
+ proxy_pass backend;
+ }
+ upstream backend {
+ {{ range nomadService "redis" }}
+ server {{ .Address }}:{{ .Port }};
+ {{ else }}server 127.0.0.1:65535; # force a 502
+ {{ end }}
+ }
+}
+EOF
+ destination = "local/nginx/nginx.conf"
+ change_mode = "signal"
+ change_signal = "SIGHUP"
+ }
+
+ resources {
+ cpu = 50
+ memory = 10
+ }
+
+ scaling "cpu" {
+ policy {
+ cooldown = "1m"
+ evaluation_interval = "10s"
+
+ check "95pct" {
+ strategy "app-sizing-percentile" {
+ percentile = "95"
+ }
+ }
+ }
+ }
+
+ scaling "mem" {
+ policy {
+ cooldown = "1m"
+ evaluation_interval = "10s"
+
+ check "max" {
+ strategy "app-sizing-max" {}
+ }
+ }
+ }
+ }
+
+ service {
+ name = "redis-lb"
+ port = "lb"
+ address_mode = "host"
+ provider = "nomad"
+ }
+ }
+
+ group "cache" {
+ count = 3
+
+ network {
+ port "db" {
+ to = 6379
+ }
+ }
+
+ task "redis" {
+ driver = "docker"
+
+ config {
+ image = "redis:6.0"
+ ports = ["db"]
+ }
+
+ resources {
+ cpu = 500
+ memory = 256
+ }
+
+ scaling "cpu" {
+ policy {
+ cooldown = "1m"
+ evaluation_interval = "10s"
+
+ check "95pct" {
+ strategy "app-sizing-percentile" {
+ percentile = "95"
+ }
+ }
+ }
+ }
+
+ scaling "mem" {
+ policy {
+ cooldown = "1m"
+ evaluation_interval = "10s"
+
+ check "max" {
+ strategy "app-sizing-max" {}
+ }
+ }
+ }
+
+ service {
+ name = "redis"
+ port = "db"
+ address_mode = "host"
+ provider = "nomad"
+ }
+ }
+ }
+}
+
job "das-load-test" {
+ datacenters = ["dc1"]
+ type = "batch"
+
+ parameterized {
+ payload = "optional"
+ meta_optional = ["requests", "clients"]
+ }
+
+ group "redis-benchmark" {
+ task "redis-benchmark" {
+ driver = "docker"
+
+ config {
+ image = "redis:6.0"
+ command = "redis-benchmark"
+
+ args = [
+ "-h",
+ "${HOST}",
+ "-p",
+ "${PORT}",
+ "-n",
+ "${REQUESTS}",
+ "-c",
+ "${CLIENTS}",
+ ]
+ }
+
+ template {
+ destination = "secrets/env.txt"
+ env = true
+
+ data = <<EOF
+{{ with nomadService "redis-lb" }}{{ with index . 0 -}}
+HOST={{.Address}}
+PORT={{.Port}}
+{{- end }}{{ end }}
+REQUESTS={{ or (env "NOMAD_META_requests") "100000" }}
+CLIENTS={{ or (env "NOMAD_META_clients") "50" }}
+EOF
+ }
+
+ resources {
+ cpu = 100
+ memory = 128
+ }
+ }
+ }
+}
+
job "nginx" {
+ datacenters = ["dc1"]
+
+ group "nginx" {
+
+ constraint {
+ attribute = "${attr.unique.hostname}"
+ value = "slave0"
+ }
+
+ #Vault tls가 있고 nomad client hcl 파일에 host volume이 명시되어 있는 설정 값
+ volume "cert-data" {
+ type = "host"
+ source = "cert-data"
+ read_only = false
+ }
+
+ #실패 없이 되라고 행운의 숫자인 7을 4번 줌
+ network {
+ port "http" {
+ to = 7777
+ static = 7777
+ }
+ }
+
+ service {
+ name = "nginx"
+ port = "http"
+ }
+
+ task "nginx" {
+ driver = "docker"
+
+ volume_mount {
+ volume = "cert-data"
+ destination = "/usr/local/cert"
+ }
+
+ config {
+ image = "nginx"
+
+ ports = ["http"]
+ volumes = [
+ "local:/etc/nginx/conf.d",
+
+ ]
+ }
+ template {
+ data = <<EOF
+#Vault는 active서버 1대외에는 전부 standby상태이며
+#서비스 호출 시(write)에는 active 서비스만 호출해야함으로 아래와 같이 consul에서 서비스를 불러옴
+
+upstream backend {
+{{ range service "active.vault" }}
+ server {{ .Address }}:{{ .Port }};
+{{ else }}server 127.0.0.1:65535; # force a 502
+{{ end }}
+}
+
+server {
+ listen 7777 ssl;
+ #위에서 nomad host volume을 mount한 cert를 가져옴
+ ssl on;
+ ssl_certificate /usr/local/cert/vault/global-client-vault-0.pem;
+ ssl_certificate_key /usr/local/cert/vault/global-client-vault-0-key.pem;
+ #vault ui 접근 시 / -> /ui redirect되기 때문에 location이 /외에는 되지 않는다.
+ location / {
+ proxy_pass https://backend;
+ }
+}
+EOF
+
+ destination = "local/load-balancer.conf"
+ change_mode = "signal"
+ change_signal = "SIGHUP"
+ }
+ resources {
+ cpu = 100
+ memory = 201
+ }
+ }
+ }
+}
+
log_level = "DEBUG"
옵션 설정target "aws-asg"
설정방법 policy
의 cooldown, evaluation_interval 값을 워크로드 특성에 맞게 적절하게 설정check "mem_allocated_percentage"
check "cpu_allocated_percentage"
locals {
+ autoscaler_ver = "0.3.3"
+ #autoscaler_ver = "0.3.5"
+}
+
+job "autoscalerEnt" {
+ datacenters = ["dc1"]
+
+ group "autoscalerEnt" {
+ count = 1
+
+ network {
+ port "http" {}
+ }
+
+ task "autoscaler" {
+ // docker 기반의 Nomad Autoscaler는 다음과 같이 설정
+ // driver = "docker"
+ // config {
+ // image = "hashicorp/nomad-autoscaler-enterprise:0.3.3"
+ // command = "nomad-autoscaler"
+ // args = [
+ // "agent",
+ // "-config",
+ // "$${NOMAD_TASK_DIR}/config.hcl",
+ // "-policy-dir",
+ // "$${NOMAD_TASK_DIR}/policies/",
+ // ]
+ // ports = ["http"]
+ // }
+ driver = "exec"
+
+ config {
+ command = "/usr/local/bin/nomad-autoscaler"
+ args = [
+ "agent",
+ "-config",
+ "$${NOMAD_TASK_DIR}/config.hcl",
+ "-http-bind-address",
+ "0.0.0.0",
+ "-http-bind-port",
+ "$${NOMAD_PORT_http}",
+ "-policy-dir",
+ "$${NOMAD_TASK_DIR}/policies/",
+ ]
+ }
+
+ artifact {
+ source = "https://releases.hashicorp.com/nomad-autoscaler/${local.autoscaler_ver}+ent/nomad-autoscaler_${local.autoscaler_ver}+ent_linux_amd64.zip"
+ destination = "/usr/local/bin"
+ }
+ template {
+ data = <<EOF
+nomad {
+ address = "http://{{env "attr.unique.network.ip-address" }}:4646" #Adding nomad server addresss
+ token = "<#Adding nomad server token >"
+}
+
+apm "nomad-apm" {
+ driver = "nomad-apm"
+ config = {
+ address = "http://{{env "attr.unique.network.ip-address" }}:4646"
+ }
+}
+
+dynamic_application_sizing {
+ // Lower the evaluate interval so we can reproduce recommendations after only
+ // 5 minutes, rather than having to wait for 24hrs as is the default.
+ evaluate_after = "5m"
+}
+
+#log_level = "DEBUG"
+
+target "aws-asg" {
+ driver = "aws-asg"
+ config = {
+ aws_region = "{{ $x := env "attr.platform.aws.placement.availability-zone" }}{{ $length := len $x |subtract 1 }}{{ slice $x 0 $length}}"
+ }
+}
+
+strategy "target-value" {
+ driver = "target-value"
+}
+
+ EOF
+ destination = "$${NOMAD_TASK_DIR}/config.hcl"
+ }
+ template {
+ data = <<EOF
+scaling "cluster_policy_media" {
+ enabled = true
+ min = 1
+ max = 100
+
+ policy {
+ cooldown = "5m"
+ evaluation_interval = "20s"
+
+ check "mem_allocated_percentage" {
+ source = "nomad-apm"
+ query = "percentage-allocated_memory"
+ strategy "target-value" {
+ target = 82
+ }
+ }
+
+ // check "cpu_allocated_percentage" {
+ // source = "nomad-apm"
+ // query = "percentage-allocated_cpu"
+
+ // strategy "target-value" {
+ // target = 80
+ // }
+ // }
+
+ target "aws-asg" {
+ dry-run = "false"
+ aws_asg_name = "nomad_client_media_autoscaler"
+ node_class = "client_web"
+ node_drain_deadline = "3m"
+ node_purge = "true"
+ }
+ }
+}
+
+EOF
+ destination = "$${NOMAD_TASK_DIR}/policies/hashistack.hcl"
+ }
+
+ resources {
+ cpu = 200
+ memory = 256
+ }
+
+ service {
+ name = "autoscaler"
+ port = "http"
+
+ check {
+ type = "http"
+ path = "/v1/health"
+ interval = "5s"
+ timeout = "2s"
+ }
+ }
+ }
+ }
+}
+
# nomad var put {path기반의 varialbes} key=vaule
+$ nomad var put code/config password=password
+
job "010-code-server" {
+ datacenters = ["dc1"]
+ type = "service"
+
+ group "code-server" {
+ count = 1
+
+ network {
+ port "http" {
+ to = 8443
+ static = 8443
+ }
+ }
+
+ service {
+ name = "code-server"
+ port = "http"
+ provider = "nomad"
+
+ check {
+ type = "http"
+ path = "/"
+ interval = "2s"
+ timeout = "30s"
+ }
+ }
+
+ task "code-server-runner" {
+ driver = "docker"
+
+ template {
+ data = <<EOH
+{{ with nomadVar "code/config" }}
+PASSWORD={{ .password }}
+SUDO_PASSWORD={{ .password }}
+{{ end }}
+EOH
+
+ destination = "secrets/file.env"
+ env = true
+ }
+
+
+ config {
+ image = "linuxserver/code-server"
+ ports = ["http"]
+ }
+
+ env {
+ PGID=1000
+ PUID=1000
+ }
+
+
+ resources {
+ cpu = 1000
+ memory = 900
+ }
+ }
+ }
+}
+
+
job "elastic" {
+ datacenters = ["dc1"]
+
+ group "elastic" {
+ network {
+ port "db" {
+ static = 9200
+ }
+ port "kibana" {
+ static = 5601
+ }
+ }
+
+ service {
+ port = "db"
+
+ check {
+ type = "tcp"
+ interval = "10s"
+ timeout = "2s"
+ }
+ }
+
+ task "elasticsearch" {
+ driver = "docker"
+
+ config {
+ image = "docker.elastic.co/elasticsearch/elasticsearch:6.8.9"
+ ports = ["db"]
+ mount = [
+ {
+ type = "bind"
+ source = "local/elasticsearch.yml"
+ target = "/usr/share/elasticsearch/config/elasticsearch.yml"
+ }
+ ]
+ }
+
+ template {
+ data = <<EOH
+network.host: 0.0.0.0
+discovery.seed_hosts: ["127.0.0.1"]
+xpack.security.enabled: true
+xpack.license.self_generated.type: trial
+xpack.monitoring.collection.enabled: true
+EOH
+ destination = "local/elasticsearch.yml"
+ }
+
+ env {
+ # "discovery.type":"single-node",
+ ES_JAVA_OPTS = "-Xms512m -Xmx1024m"
+ }
+
+ resources {
+ cpu = 2000
+ memory = 1024
+ }
+ }
+
+ task "kibana" {
+ driver = "docker"
+
+ config {
+ image = "docker.elastic.co/kibana/kibana:6.8.9"
+ ports = ["kibana"]
+ mount = [
+ {
+ type = "bind"
+ source = "local/kibana.yml"
+ target = "/usr/share/kibana/config/kibana.yml"
+ }
+ ]
+ }
+
+ template {
+ data = <<EOH
+server.name: kibana
+elasticsearch.username: elastic
+elasticsearch.password: elastic
+EOH
+ destination = "local/kibana.yml"
+ }
+
+ env {
+ ELASTICSEARCH_URL="http://172.28.128.31:9200"
+ }
+
+ resources {
+ cpu = 1000
+ memory = 1024
+ }
+ }
+ }
+}
+
+
job "elk" {
+
+ datacenters = ["dc1"]
+
+ constraint {
+ attribute = "${attr.kernel.name}"
+ value = "linux"
+ }
+
+ update {
+ stagger = "10s"
+ max_parallel = 1
+ }
+
+ group "elk" {
+ count = 1
+
+ restart {
+ attempts = 2
+ interval = "1m"
+ delay = "15s"
+ mode = "delay"
+ }
+ network {
+ port "elastic" {
+ to = 9200
+ static = 9200
+ }
+ port "kibana" {
+ to = 5601
+ }
+ port "logstash" {
+ to = 5000
+ }
+ }
+
+ task "elasticsearch" {
+ driver = "docker"
+
+ constraint {
+ attribute = "${attr.unique.hostname}"
+ value = "slave2"
+ }
+
+ config {
+ image = "elasticsearch:7.16.2"
+ ports = ["elastic"]
+ volumes = [
+ "local/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml",
+ ]
+ }
+ template {
+ data = <<EOF
+cluster.name: "my-cluster"
+network.host: 0.0.0.0
+discovery.type: single-node
+discovery.seed_hosts: ["127.0.0.1"]
+xpack.security.enabled: true
+xpack.license.self_generated.type: trial
+xpack.monitoring.collection.enabled: true
+EOF
+ destination = "local/elasticsearch.yml"
+ change_mode = "signal"
+ change_signal = "SIGHUP"
+ }
+ env {
+ ELASTIC_PASSWORD = "elastic"
+ }
+
+ service {
+ name = "${TASKGROUP}-elasticsearch"
+ port = "elastic"
+ }
+
+ resources {
+ cpu = 500
+ memory = 2048
+ }
+ }
+
+ task "kibana" {
+ driver = "docker"
+
+ constraint {
+ attribute = "${attr.unique.hostname}"
+ value = "slave2"
+ }
+
+ config {
+ image = "kibana:7.16.2"
+ ports = ["kibana"]
+ volumes = [
+ "local/kibana.yml:/usr/share/kibana/config/kibana.yml"
+ ]
+ }
+ template {
+ data = <<EOF
+#
+# ** THIS IS AN AUTO-GENERATED FILE **
+#
+
+# Default Kibana configuration for docker target
+server.host: "0.0.0.0"
+server.shutdownTimeout: "5s"
+elasticsearch.hosts: [ "http://{{ env "NOMAD_IP_elk" }}:{{ env "NOMAD_PORT_elk" }}" ]
+elasticsearch.username: elastic
+elasticsearch.password: elastic
+EOF
+
+ destination = "local/kibana.yml"
+ change_mode = "signal"
+ change_signal = "SIGHUP"
+ }
+
+ service {
+ name = "${TASKGROUP}-kibana"
+ port = "kibana"
+ check {
+ type = "http"
+ path = "/"
+ interval = "10s"
+ timeout = "2s"
+ }
+ }
+
+ resources {
+ cpu = 500
+ memory = 1200
+ }
+ }
+
+ task "logstash" {
+ driver = "docker"
+
+ constraint {
+ attribute = "${attr.unique.hostname}"
+ value = "slave2"
+ }
+
+ config {
+ image = "logstash:7.16.2"
+ ports = ["logstash"]
+ volumes = [
+ "local/logstash.yml:/usr/share/logstash/config/logstash.yml"
+ ]
+ }
+ template {
+ data = <<EOF
+http.host: "0.0.0.0"
+xpack.monitoring.elasticsearch.hosts: [ "http://{{ env "NOMAD_IP_elk" }}:{{ env "NOMAD_PORT_elk" }}" ]
+EOF
+
+ destination = "local/logstash.yml"
+ change_mode = "signal"
+ change_signal = "SIGHUP"
+ }
+
+ service {
+ name = "${TASKGROUP}-logstash"
+ port = "logstash"
+ }
+
+ resources {
+ cpu = 200
+ memory = 1024
+ }
+ }
+ }
+}
+
+
job "22-fastapi" {
+ datacenters = ["dc1"]
+
+ group "fastapi" {
+
+ network {
+ mode = "bridge"
+ #service가 80으로 뜸, 만약 다른 포트로 뜨는 서비스를 사용 할 경우 image와 to만 변경
+ port "http" {
+ to = 80
+ }
+ }
+
+ service {
+ name = "fastapi"
+ #여기서 port에 위에서 미리 선언한 http를 쓸 경우 다이나믹한 port를 가져오는데
+ #그럴 경우 ingress gateway에서 못 읽어 온다.
+ port = "80"
+ connect {
+ sidecar_service{}
+ }
+ }
+
+ task "fastapi" {
+ driver = "docker"
+
+ config {
+ image = "tiangolo/uvicorn-gunicorn-fastapi"
+ ports = ["http"]
+ }
+
+ resources {
+ cpu = 500
+ memory = 282
+ }
+ }
+ scaling {
+ enabled = true
+ min = 1
+ max = 3
+
+ policy {
+ evaluation_interval = "5s"
+ cooldown = "1m"
+ #driver = "nomad-apm"
+ check "mem_allocated_percentage" {
+ source = "nomad-apm"
+ query = "max_memory"
+
+ strategy "target-value" {
+ target = 80
+ }
+ }
+ }
+ }
+ }
+}
+
+
Kind = "service-defaults"
+Name = "fastapi"
+Namespace = "default"
+Protocol = "http"
+
job "ingress-demo" {
+
+ datacenters = ["dc1"]
+
+ group "ingress-group" {
+
+ network {
+ mode = "bridge"
+ #backend job인 fastapi의 port를 넣어줌
+ port "inbound" {
+ to = 80
+ }
+ }
+
+ service {
+ name = "my-ingress-service"
+ port = "inbound"
+
+ connect {
+ gateway {
+
+ proxy {
+ }
+ ingress {
+ listener {
+ #backend job인 fastapi의 port를 넣어줌
+ port = 80
+ #protocol = "http"
+ protocol = "tcp"
+ service {
+ name = "fastapi"
+ # hosts = ["*"]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
참고 : https://discuss.hashicorp.com/t/escape-characters-recognized-as-a-variable-in-template-stanza/40525
Nomad를 통해 Ops작업을 수행할 때 sysbatch
타입의 Job에 Ansible을 raw_exec
로 실행하면 전체 노드에서 일괄로 작업을 수행할 수 있다.
Ansible에서 사용하는 문법 중 {{}}
의 부호로 팩트를 사용하는 경우 Nomad에서 사용하는 Template의 {{}}
과 겹쳐 오류가 발생한다.
Template failed: (dynamic): parse: template: :23: function "ansible_distribution_release" not defined
+
이경우 Nomad에서 다음과 같이 표기하여 템플릿 문자에 대한 치환이 가능하다.
{{ --> {{ "{{" }}
+ }} --> {{ "}}" }}
+
다음은 Ansible에서 apt_repository
수행 시 ansible_architecture
와 ansible_distribution_release
같은 팩트 값을 Template으로 Playbook을 작성한 예제 이다.
job "install-ansible-docker" {
+ datacenters = ["hashitalks-kr"] # 사용할 데이터 센터 이름으로 수정
+
+ type = "sysbatch" # 배치 작업 유형
+
+ constraint {
+ attribute = "${attr.os.name}"
+ value = "ubuntu"
+ }
+
+ parameterized {
+ payload = "forbidden"
+ }
+
+ group "install- group" {
+
+ task "install-ansible-task" {
+ lifecycle {
+ hook = "prestart"
+ sidecar = false
+ }
+
+ driver = "raw_exec" # 외부 스크립트를 실행
+
+ config {
+ command = "local/install_ansible.sh"
+ }
+
+ template {
+ destination = "local/install_ansible.sh"
+ data = <<EOF
+#!/bin/bash
+sudo apt-get update
+sudo apt-get install -y ansible
+EOF
+ }
+ }
+
+ task "install-docker-task" {
+ driver = "raw_exec" # 외부 스크립트를 실행
+
+ config {
+ command = "ansible-playbook"
+ args = [
+ "local/playbook.yml"
+ ]
+ }
+
+ env {
+ JAVA_VERSION = "${NOMAD_META_DesiredJavaVersion}"
+ }
+
+ template {
+ destination = "local/playbook.yml"
+ data = <<EOF
+---
+- hosts:
+ - localhost
+ become: yes
+ tasks:
+ - name: Install aptitude
+ apt:
+ name: aptitude
+ state: latest
+ update_cache: true
+
+ - name: Install required packages
+ apt:
+ pkg:
+ - apt-transport-https
+ - ca-certificates
+ - curl
+ - software-properties-common
+ - python3-pip
+ - virtualenv
+ - python3-setuptools
+ state: latest
+ update_cache: true
+
+ - name: Add Docker GPG apt Key
+ apt_key:
+ url: https://download.docker.com/linux/ubuntu/gpg
+ state: present
+
+ - name: Add Docker repository
+ apt_repository:
+ repo: "deb [arch={{ env "attr.cpu.arch" }}] https://download.docker.com/linux/ubuntu {{"{{"}} ansible_lsb.codename {{"}}"}} stable"
+ state: present
+ update_cache: true
+
+ - name: Update the apt package index
+ apt:
+ update_cache: true
+
+ - name: Install Docker CE
+ apt:
+ name: docker-ce
+ state: latest
+
+ - name: Install Docker CE etc.
+ apt:
+ name:
+ - docker-ce-cli
+ - containerd.io
+ - docker-buildx-plugin
+ - docker-compose-plugin
+ state: present
+
+ - name: Ensure Docker starts on boot
+ service:
+ name: docker
+ enabled: true
+ state: started
+EOF
+ }
+
+ resources {
+ cpu = 500
+ memory = 256
+ }
+ }
+ }
+}
+
image info : https://quay.io/repository/wildfly/wildfly?tab=info
github : https://github.com/jboss-dockerfiles/wildfly
wildfly docker example : http://www.mastertheboss.com/soa-cloud/docker/deploying-applications-on-your-docker-wildfly-image/
Wildfly 이미지를 베이스로 기존 Dockerfile을 작성하여 빌드 후 컨테이너를 기준으로 배포하는 것도 가능하지만, 베이스 이미지를 유지한 채로 애플리케이션(war)을 바인드하여 실행하는 것도 가능하다.
dockerfile 의 예
FROM jboss/wildfly
+RUN /opt/jboss/wildfly/bin/add-user.sh admin admin --silent
+ADD jboss-as-helloworld.war /opt/jboss/wildfly/standalone/deployments/
+CMD ["/opt/jboss/wildfly/bin/standalone.sh", "-b", "0.0.0.0", "-bmanagement", "0.0.0.0"]
+
FROM
은 Nomad가 실행시킬 이미지로 지정RUN
절의 add-user.sh
는 mgmt-users.properties
를 생성하고자 하는 목적이므로 Nomad의 artifact
로 중앙레포에서 받거나 template
으로 작성하여 바인딩 가능ADD
절은 WAR파일을 추가하는 것으로 호스트의 파일 또는 artifact
로 중앙레포에서 받아 바인딩CMD
절은 기본 실행명령을 대체하는 것으로 Nomad에서 command
와 args
로 대체 가능job > groups > task(docker) > artifact :
WAR 파일을 다운로드 받아 준비
참고
Nomad 클라이언트 호스트에 미리 파일을 두는것도 가능하나 오케스트레이션 특성상 중랑 레포기능을 하는곳에서 배포시 다운받는 방식이 확장성 측면에서 고려되어야 함
job > groups > task(docker) > template :
add-user.sh
를 통해 management 콘솔의 사용자를 생성해야 하지만 미리 생성된 내용(admin/admin)을 넣어 처리# example (admin/admin)
+/opt/jboss/wildfly/bin/add-user.sh admin admin --silent
+cat /opt/jboss/wildfly/standalone/configuration/mgmt-users.properties
+
admin=c22052286cd5d72239a90fe193737253
+
job > groups > task(docker) > config > mount :
mgmt-users.properties
를 컨테이너에 바인딩팁
volumes
로 처리하는것도 가능 https://www.nomadproject.io/docs/drivers/docker#volumes
job > groups > task(docker) > config > args :
management가 기본 127.0.0.1
이므로 포트포워딩으로 접속이 불가하므로 0.0.0.0
으로 변경
args = ["/opt/jboss/wildfly/bin/standalone.sh", "-b", "0.0.0.0", "-bmanagement", "0.0.0.0"]
+
팁
조금더 명확하게는 command
에 "/opt/jboss/wildfly/bin/standalone.sh"
를 구성하고 args를 분리하는 것도 가능
locals {
+ WAR_URL = "https://github.com/spagop/quickstart/raw/master/management-api-examples/mgmt-deploy-application/application/jboss-as-helloworld.war"
+ WAR_DEST = "local/deployments/jboss-as-helloworld.war"
+}
+
+job "jboss" {
+ datacenters = ["dc1"]
+
+ group "jboss" {
+
+ network {
+ port "http" {
+ to = "8080"
+ }
+ port "mgmt" {
+ to = "9990"
+ }
+ }
+
+ ### csi (nfs)
+ # volume "nfs-vol" {
+ # type = "csi"
+ # source = "nfs-vol"
+ # read_only = false
+ # attachment_mode = "file-system"
+ # access_mode = "single-node-writer"
+ # #per_alloc = true
+ # }
+
+ service {
+ name = "jboss-http"
+ port = "http"
+
+ check {
+ type = "tcp"
+ interval = "10s"
+ timeout = "2s"
+ }
+ }
+
+ task "http" {
+ driver = "docker"
+
+ config {
+ image = "jboss/wildfly"
+ ports = ["http", "mgmt"]
+ args = ["/opt/jboss/wildfly/bin/standalone.sh", "-b", "0.0.0.0", "-bmanagement", "0.0.0.0"]
+ mount {
+ type = "bind"
+ target = "/opt/jboss/wildfly/standalone/deployments/jboss-as-helloworld.war"
+ source = "local/deployments/jboss-as-helloworld.war"
+ readonly = false
+ bind_options {
+ propagation = "rshared"
+ }
+ }
+ mount {
+ type = "bind"
+ target = "/opt/jboss/wildfly/standalone/configuration/mgmt-users.properties"
+ source = "local/configuration/mgmt-users.properties"
+ }
+ }
+
+ env {
+ }
+
+ artifact {
+ source = local.WAR_URL
+ destination = local.WAR_DEST
+ }
+
+ template {
+ # mgmt : admin / admin
+ data = "admin=c22052286cd5d72239a90fe193737253"
+ destination = "local/configuration/mgmt-users.properties"
+ }
+
+ resources {
+ cpu = 500
+ memory = 512
+ }
+
+ ### csi (nfs)
+ # volume_mount {
+ # volume = "nfs-vol"
+ # destination = "/csi"
+ # }
+ }
+
+ scaling {
+ enabled = true
+ min = 1
+ max = 4
+
+ ### cpu autoscale
+ # policy {
+ # evaluation_interval = "10s"
+ # cooldown = "1m"
+ # driver = "nomad-apm"
+ # check "mem_allocated_percentage" {
+ # source = "nomad-apm"
+ # query = "avg_cpu"
+ # strategy "target-value" {
+ # target = 90
+ # }
+ # }
+ # }
+ }
+ }
+}
+
Nomad
- Java Driver : https://developer.hashicorp.com/nomad/docs/drivers/java
- Schecuduler Config : https://developer.hashicorp.com/nomad/api-docs/operator/scheduler
Jenkins의 공식 설치 안내에서 처럼 Java를 실행시킬 수 있는 환경에서 war
형태의 자바 웹어플리케이션 압축 파일을 실행하는 형태, 컨테이너, OS별 지원되는 패키지 형태로 실행된다.
각 설치 형태의 특징은 다음과 같다.
형태 | 설명 | 특징 |
---|---|---|
war | JRE 또는 JDK가 설치되어있는 환경에서 실행가능 | 서버 재부팅, 장애 시 수동으로 실행 필요 |
Container | 컨테이너 런타임(e.g. docker, containerd ... )에서 실행 | 컨테이너 관리 및 영구저장소 관리 필요 |
Package | 각 OS 마다 제공되는 패키지 관리자에서 관리 | 필요한 패키지 자동설치 및 재부팅시 재시작 가능하나 장애시 수동 실행 필요 |
Nomad는 애플리케이션 실행을 오케스트레이션 해주는 역할로, Java 애플리케이션의 여러 실행 형태에 장점만을 취합하여 실행 환경을 제공할 수 있다.
Nomoad 에서는 Jenkins 실행을 위해 다음 조건이 필요하다.
# 예시
+mkdir -p /opt/nomad/volume/jenkins
+
nobody
로 부여하므로 nobody
계정에 권한으로 실행 필요# 예시
+# macOS인 경우
+chown -R nobody:nobody /opt/nomad/volume/jenkins
+# ubuntu인 경우
+chown -R nobody:nogroup /opt/nomad/volume/jenkins
+
client
블록에 Host Volume을 지정client {
+ enabled = true
+ # 생략
+ host_volume "jenkins" {
+ path = "/opt/nomad/volume/jenkins"
+ read_only = false
+ }
+}
+
Nomad 1.1부터 리소스에 대한 유연한 설정 기능이 추가되었다. 그 중 메모리 할당 추가 기능인 Memory Oversubscription이 추가되었고, Java 애플리케이션이 갖는 특징인 JVM 메모리의 범위 할당과도 연계되는 설정 방식이다. JVM 64bit 부터는 기본적으로 소모하는 메모리가 크고, 한번 증가한 메모리는 장기간 유지되기 때문에 메모리에 대한 유연한 설정은 중요하다.
특히, Nomad에서 리소스를 격리하여 Java 드라이버에 제공되므로 지정된 메모리보다 초과 사용하는 경우 격리된 리소스로 인해 Error code 143 Signal 9
(Out Of Memory 로 인한 프로세스 강제종료)형상이 발생할 수 있다.
Oversubscription
기능은 고급 기능으로 구성설정에서 지정할 수는 없고 CLI/API/Terraform을 사용하여 설정을 변경해야 한다. (설명 링크)
링크 : https://developer.hashicorp.com/nomad/docs/commands/operator/scheduler/set-config
$ nomad operator scheduler set-config -memory-oversubscription=true
+
$ curl -s $NOMAD_ADDR/v1/operator/scheduler/configuration | \
+jq '.SchedulerConfig | .MemoryOversubscriptionEnabled=true' | \
+curl -X PUT $NOMAD_ADDR/v1/operator/scheduler/configuration -d @-
+
링크 : https://registry.terraform.io/providers/hashicorp/nomad/latest/docs/resources/scheduler_config
resource "nomad_scheduler_config" "config" {
+ scheduler_algorithm = "binpack"
+ memory_oversubscription_enabled = true
+ preemption_config = {
+ system_scheduler_enabled = true
+ batch_scheduler_enabled = true
+ service_scheduler_enabled = true
+ sysbatch_scheduler_enabled = true
+ }
+}
+
JENKINS_HOME
선언artifact
로 받은 war파일 경로--httpPort
는 args 항목이고 jvm 옵션이 아님에 주의variable "namespace" {
+ default = "default"
+}
+
+variable "jenkins_version" {
+ default = "2.361.3"
+}
+
+job "jenkins" {
+ datacenters = ["home"]
+ namespace = var.namespace
+
+ type = "service"
+
+ constraint {
+ attribute = "${meta.type}"
+ value = "vraptor"
+ }
+
+ update {
+ healthy_deadline = "10m"
+ progress_deadline = "20m"
+ }
+
+ group "jenkins" {
+ count = 1
+ volume "jenkins" {
+ type = "host"
+ source = "jenkins"
+ read_only = false
+ }
+ network {
+ port "jenkins_http" {
+ // to = 8080
+ static = 8888
+ }
+ }
+ task "war" {
+ driver = "java"
+ resources {
+ cpu = 1000
+ memory = 1024
+ memory_max = 2048
+ }
+ env {
+ JENKINS_HOME = "/jenkins_home"
+ }
+ config {
+ jar_path = "local/jenkins.war"
+ jvm_options = ["-Xms1024m", "-Xmx2048m"]
+ args = ["--httpPort=${NOMAD_PORT_jenkins_http}"]
+ }
+ volume_mount {
+ volume = "jenkins"
+ destination = "/jenkins_home"
+ read_only = false
+ }
+ service {
+ name = "jenkins"
+ tags = ["java", "cicd"]
+ provider = "nomad"
+
+ port = "jenkins_http"
+
+ check {
+ name = "jenkins port"
+ type = "tcp"
+ interval = "10s"
+ timeout = "2s"
+ }
+ }
+ logs {
+ max_files = 10
+ max_file_size = 10
+ }
+ artifact {
+ source = "https://get.jenkins.io/war-stable/${var.jenkins_version}/jenkins.war"
+ options {
+ checksum = "sha256:f39cb8d09fd17c72dc096511ce50f245fc3004d1022aaaf60421a536f740c9b9"
+ }
+ // destination = "local"
+ }
+ }
+ }
+}
+
실행 후 Admin Token
Jenkins Home 디렉토리로 지정한 디렉토리에 관련 파일이 생성되며, 실제 호스트의 디렉토리에서 접근하는 것도 가능하나 Nomad의 Exec
에서 접근하는 것도 가능하다
Keycloak은 Stateful 한 특성이 있어서 볼륨을 붙여주는것이 좋다.
// nomad namespace apply -description "Keycloak" keycloak
+
+job "keycloak" {
+ type = "service"
+ datacenters = ["dc1"]
+ namespace = "keycloak"
+
+ group "keycloak" {
+ count = 1
+
+ volume "keycloak-vol" {
+ type = "host"
+ read_only = false
+ source = "keycloak-vol"
+ }
+
+ task "keycloak" {
+ driver = "docker"
+
+ volume_mount {
+ volume = "keycloak-vol"
+ destination = "/opt/jboss/keycloak/standalone/data"
+ read_only = false
+ }
+
+ config {
+ image = "quay.io/keycloak/keycloak:14.0.0"
+ port_map {
+ keycloak = 8080
+ callback = 8250
+ }
+ }
+
+ env {
+ KEYCLOAK_USER = "admin"
+ KEYCLOAK_PASSWORD = "admin"
+ }
+
+ resources {
+ memory = 550
+
+ network {
+ port "keycloak" {
+ static = 18080
+ }
+ port "callback" {
+ static = 18250
+ }
+ }
+ }
+
+ service {
+ name = "keycloak"
+ tags = ["auth"]
+
+ check {
+ type = "tcp"
+ interval = "10s"
+ timeout = "2s"
+ port = "keycloak"
+ }
+ }
+ }
+ }
+}
+
job "mongodb" {
+ datacenters = ["dc1"]
+
+ group "mongodb" {
+ network {
+ port "db" {
+ static = 27017
+ }
+ }
+
+ service {
+ port = "db"
+
+ check {
+ type = "tcp"
+ interval = "10s"
+ timeout = "2s"
+ }
+ }
+
+ task "mongodb" {
+ driver = "docker"
+
+ config {
+ image = "mongo:3.6.21"
+ ports = ["db"]
+ }
+
+ env {
+ MONGO_INITDB_ROOT_USERNAME = "admin"
+ MONGO_INITDB_ROOT_PASSWORD = "password"
+ }
+
+ resources {
+ cpu = 2000
+ memory = 1024
+ }
+ }
+ }
+}
+
job "nexus" {
+ datacenters = ["dc1"]
+
+ group "nexus" {
+ count = 1
+
+ network {
+ port "http" {
+ to = 8081
+ static = 8081
+ }
+ }
+
+ task "nexus" {
+ driver = "docker"
+
+ config {
+ image = "sonatype/nexus3"
+ ports = ["http"]
+ }
+
+ env {
+ INSTALL4J_ADD_VM_PARAMS = "-Xms2703m -Xmx2703m -XX:MaxDirectMemorySize=2703m -Djava.util.prefs.userRoot=/some-other-dir"
+ }
+
+ resources {
+ cpu = 1000
+ memory = 2703
+ }
+ }
+ }
+}
+
job "nginx" {
+ datacenters = ["dc1"]
+
+ group "nginx" {
+ //인증서는 host volume에 업로드
+ volume "certs" {
+ type = "host"
+ source = "certs"
+ read_only = true
+ }
+
+ network {
+ port "http" {
+ static = 80
+ to = 80
+ }
+ port "https" {
+ to = 443
+ static = 443
+ }
+ }
+
+ service {
+ name = "nginx"
+ port = "http"
+ tags = ["web"]
+ check {
+ type = "tcp"
+ port = "http"
+ interval = "2s"
+ timeout = "2s"
+ }
+ }
+
+ task "server" {
+
+ driver = "docker"
+
+ volume_mount {
+ volume = "certs"
+ destination = "/etc/nginx/certs"
+ }
+
+ config {
+ image = "nginx"
+ ports = ["http","https"]
+ #ports = ["http","https"]
+ volumes = [
+ "local:/etc/nginx/conf.d",
+ ]
+ }
+
+ template {
+ data = <<EOF
+
+upstream login.shoping.co.kr {
+{{ range service "login" }}
+ server {{ .Address }}:{{ .Port }};
+{{ else }}server 127.0.0.1:65535; # force a 502
+{{ end }}
+}
+upstream singup.shoping.co.kr {
+{{ range service "signup" }}
+ server {{ .Address }}:{{ .Port }};
+{{ else }}server 127.0.0.1:65535; # force a 502
+{{ end }}
+}
+upstream main.shoping.co.kr {
+{{ range service "main" }}
+ server {{ .Address }}:{{ .Port }};
+{{ else }}server 127.0.0.1:65535; # force a 502
+{{ end }}
+}
+
+server {
+ listen 80;
+ listen 443 ssl;
+ //domain 및 subdomain호출
+ server_name *.shoping.co.kr;
+ ssl_certificate "/etc/nginx/certs/server.pem";
+ ssl_certificate_key "/etc/nginx/certs/server.key";
+ proxy_read_timeout 300;
+ proxy_buffers 64 16k;
+
+ //각 sub도메인별
+ location / {
+ if ($host = login.shoping.co.kr) {
+ proxy_pass login.shoping.co.kr;
+ }
+ if ($host = singup.shoping.co.kr) {
+ proxy_pass singup.shoping.co.kr;
+ }
+ if ($host !~ "(.*main)") {
+ proxy_pass main.shoping.co.kr;
+ }
+ }
+}
+
+
+
+EOF
+
+ destination = "local/load-balancer.conf"
+ change_mode = "signal"
+ change_signal = "SIGHUP"
+ }
+ resources {
+ cpu = 2000
+ memory = 2000
+ }
+ }
+ }
+}
+
+
+
# Docker file
+FROM blasteh/vuepress:8.3 #기존에 돌아 다니는 vuepress의 npm 버전이 너무 낮아 하나 받아서 버전업함
+
+#특정 패키지 빌드 시 아래와 같은패캐지들을 필요로 함
+RUN apk add --no-cache python2
+RUN apk add --no-cache make
+RUN apk add --no-cache gcc
+RUN apk add --no-cache g++
+
+RUN mkdir /etc/bin
+
+RUN cp /usr/bin/python2 /etc/bin/python2.7
+RUN cp /usr/bin/make /etc/bin/make
+RUN cp /usr/bin/gcc /etc/bin/gcc
+RUN cp /usr/bin/g++ /etc/bin/g++
+
+RUN npm config set python /etc/bin/python2.7
+RUN npm config set make /etc/bin/make
+RUN npm config set gcc /etc/bin/gcc
+RUN npm config set g++ /etc/bin/g++
+
+ADD docs /root/src/docs
+
+WORKDIR /root/src/docs
+RUN npm install
+
+expose 8000
+
+ENTRYPOINT ["/usr/local/bin/npm", "run", "dev"]
+
+
+
pack/vuepres
+├── metadata.hcl
+├── outputs.tpl
+├── templates
+│ └── vuepress.nomad.tpl
+└── variables.hcl
+
#metadata.hcl
+app {
+ url = "https://gitlab.com/swbs9000/nomad-pack"
+ author = "unghee"
+}
+
+pack {
+ name = "vuepress"
+ description = "vuepress test."
+ url = "https://github.com/swbs90/vuepress"
+ version = "0.0.1"
+}
+
#variables.hcl
+variable "job_name" {
+ description = "The name to use as the job name which overrides using the pack name"
+ type = string
+ // If "", the pack name will be used
+ default = ""
+}
+
+variable "datacenters" {
+ description = "A list of datacenters in the region which are eligible for task placement"
+ type = list(string)
+ default = ["dc1"]
+}
+
+variable "region" {
+ description = "The region where the job should be placed"
+ type = string
+ default = "global"
+}
+
+variable "consul_service_name" {
+ description = "The consul service you wish to load balance"
+ type = string
+ default = "webapp"
+}
+
+variable "version_tag" {
+ description = "The docker image version. For options, see https://hub.docker.com/repository/docker/swbs90/vuepress"
+ type = string
+ default = "latest"
+}
+
+#variable "http_port" {
+# description = "The Nomad client port that routes to the Vuepress. This port will be where you visit your load balanced application"
+# type = number
+# default = 8000
+#}
+
+variable "resources" {
+ description = "The resource to assign to the Vuepress system task that runs on every client"
+ type = object({
+ cpu = number
+ memory = number
+ })
+ default = {
+ cpu = 800,
+ memory = 1200
+ }
+}
+
+
#vuepress.nomad.tpl
+job "[[ .vuepress.job_name ]]" {
+ region = "[[ .vuepress.region ]]"
+ datacenters = [[ .vuepress.datacenters | toPrettyJson ]]
+ // must have linux for network mode
+ constraint {
+ attribute = "${attr.kernel.name}"
+ value = "linux"
+ }
+
+ group "vuepress" {
+ count = 1
+ network {
+ port "http" {
+ to = 8000
+ }
+ }
+
+ service {
+ name = "[[ .vuepress.consul_service_name ]]"
+ port = "http"
+ }
+
+ task "vuepress" {
+ driver = "docker"
+ config {
+ image = "swbs90/vuepress:[[ .vuepress.version_tag ]]"
+ ports = ["http"]
+ }
+ resources {
+ cpu = [[ .vuepress.resources.cpu ]]
+ memory = [[ .vuepress.resources.memory ]]
+ }
+ }
+ }
+}
+
+
#커스텀 registry 등록하기
+nomad-pack registry add vuepress https://gitlab.com/swbs9000/vuepress.git
+
+#배포하기
+nomad-pack plan vuepress --var="job_name=vuepress" --var="consul_service_name=vuepress" --var="version_tag=0.0.3" --registry=vuepress
++/- Job: "vuepress"
+- Meta[pack.deployment_name]: "vuepress@latest"
+- Meta[pack.job]: "vuepress"
+- Meta[pack.name]: "vuepress"
+- Meta[pack.path]: "/root/.nomad/packs/vuepress/vuepress@latest"
+- Meta[pack.registry]: "vuepress"
+- Meta[pack.version]: "latest"
+Task Group: "vuepress" (1 create/destroy update)
+ Task: "vuepress"
+
+» Scheduler dry-run:
+- All tasks successfully allocated.
+Plan succeeded
+
+nomad-pack run vuepress --var="job_name=vuepress" --var="consul_service_name=vuepress" --var="version_tag=0.0.3" --registry=vuepress
+ Evaluation ID: d38e6717-cd12-6ef5-62d4-9b5da1755020
+ Job 'vuepress' in pack deployment 'vuepress@latest' registered successfully
+Pack successfully deployed. Use vuepress@latest with --ref=latest to manage this this deployed instance with plan, stop, destroy, or info
+
+Vuepress(my docma) successfully deployed.
+
+
job "oracle" {
+ datacenters = ["dc1"]
+
+ group "oracle" {
+ network {
+ port "db" {
+ static = 1521
+ }
+ port "manage" {
+ static = 5500
+ }
+ }
+
+ service {
+ port = "db"
+
+ check {
+ type = "tcp"
+ interval = "10s"
+ timeout = "2s"
+ }
+ }
+
+ task "oracle" {
+ driver = "docker"
+
+ config {
+ image = "oracle/database:18.4.0-xe"
+ ports = ["db", "manage"]
+ }
+
+ env {
+ DB_MEMORY = "2GB"
+ ORACLE_PWD = "password"
+ ORACLE_SID = "XE"
+ }
+
+ resources {
+ cpu = 2000
+ memory = 1024
+ }
+ }
+ }
+}
+
job "24-paramete" {
+ datacenters = ["dc1"]
+ type = "batch"
+
+ parameterized {
+ payload = "forbidden"
+ meta_required = ["room_num"]
+ }
+
+ group "run-main-job" {
+
+ task "run-main-job" {
+ driver = "raw_exec"
+
+ config {
+ command = "nomad"
+ # arguments
+ args = ["job", "run", "${NOMAD_TASK_DIR}/room.job" ]
+ }
+ template {
+ data = <<EOH
+
+#####################
+
+job "{{ env "NOMAD_META_room_num" }}" {
+ datacenters = ["dc1"]
+
+ group "jboss" {
+
+ network {
+ port "http" {
+ to = "8080"
+ }
+ }
+ service {
+ port = "http"
+ name = "{{ env "NOMAD_META_room_num" }}"
+ check {
+ type = "tcp"
+ interval = "10s"
+ timeout = "2s"
+ }
+ }
+ task "http" {
+ driver = "docker"
+ config {
+ image = "jboss/wildfly"
+ ports = ["http"]
+ }
+ resources {
+ cpu = 500
+ memory = 282
+ }
+ }
+ }
+}
+
+EOH
+ destination = "local/room.job"
+ }
+ }
+ }
+}
+
job "redis-cluster" {
+
+ datacenters = ["dc1"]
+
+ group "redis" {
+
+ volume "redis-data" {
+ type = "host"
+ source = "redis-data"
+ read_only = false
+ }
+
+ volume "redis-cluster" {
+ type = "host"
+ source = "redis-cluster"
+ read_only = false
+ }
+
+ network {
+
+ port "master" {
+ to = 6379
+ }
+ port "slave" {
+ to = 6380
+ }
+ }
+ service {
+ name = "master-redis"
+ tags = ["master-redis"]
+ port = "master"
+ check {
+ port = "master"
+ type = "tcp"
+ interval = "10s"
+ timeout = "2s"
+ }
+ }
+
+ service {
+ name = "slave-redis"
+ tags = ["slave-redis"]
+ port = "slave"
+ check {
+ port = "slave"
+ type = "tcp"
+ interval = "10s"
+ timeout = "2s"
+ }
+ }
+
+
+
+ task "redis-master" {
+ volume_mount {
+ volume = "redis-data"
+ destination = "/data"
+ }
+ volume_mount {
+ volume = "redis-cluster"
+ destination = "/master"
+ }
+ driver = "docker"
+ config {
+ network_mode = "host"
+ image = "redis:5.0.5-buster"
+ ports = ["master"]
+ command = "redis-server"
+ args = [
+ "${NOMAD_TASK_DIR}/redis.conf"
+ ]
+ }
+ template {
+ data = <<EOF
+port {{ env "NOMAD_PORT_master" }}
+bind {{ env "NOMAD_IP_master" }}
+#bind 127.0.0.1 ::1
+cluster-enabled yes
+cluster-config-file /master/nodes.conf
+cluster-node-timeout 3000
+appendonly yes
+
+EOF
+
+ destination = "local/redis.conf"
+ change_mode = "signal"
+ change_signal = "SIGHUP"
+ }
+
+ resources {
+ cpu = 1000
+ memory = 1001
+ }
+ }
+ task "redis-slave" {
+
+ volume_mount {
+ volume = "redis-data"
+ destination = "/data"
+ }
+ volume_mount {
+ volume = "redis-cluster"
+ destination = "/slave"
+ }
+ env {
+ NODE_IP = "${NOMAD_IP_slave-redis}"
+ }
+ driver = "docker"
+ config {
+ network_mode = "host"
+ image = "redis:5.0.5-buster"
+ ports = ["slave"]
+ command = "redis-server"
+ args = [
+ "${NOMAD_TASK_DIR}/redis.conf"
+ ]
+ }
+ template {
+ data = <<EOF
+port {{ env "NOMAD_PORT_slave" }}
+bind {{ env "NOMAD_IP_slave" }}
+#bind 127.0.0.1 ::1
+cluster-enabled yes
+cluster-config-file /slave/nodes.conf
+cluster-node-timeout 3000
+appendonly yes
+
+EOF
+
+ destination = "local/redis.conf"
+ change_mode = "signal"
+ change_signal = "SIGHUP"
+ }
+
+ resources {
+ cpu = 1000
+ memory = 1001
+ }
+ }
+ }
+}
+
+
+
+
+
system
타입으로 모든 노드에서 실행되도록 구성nomad namespace apply -description "scouter" scouter
Volume
할당하는 것을 권장 variable "version" {
+ default = "2.15.0"
+ description = "scouter의 버전 기입 또는 배포시 입력 받기"
+}
+
+locals {
+ souter_release_url = "https://github.com/scouter-project/scouter/releases/download/v${var.version}/scouter-min-${var.version}.tar.gz"
+}
+
+job "scouter-collector" {
+ datacenters = ["dc1"]
+ // namespace = "scouter"
+
+ type = "service"
+
+ group "collector" {
+ count = 1
+
+ scaling {
+ enabled = false
+ min = 1
+ max = 1
+ }
+
+ task "collector" {
+ driver = "java"
+ resources {
+ network {
+ port "collector" {
+ to = 6100
+ static = 6100
+ }
+ }
+ cpu = 500
+ memory = 512
+ }
+ artifact {
+ source = local.souter_release_url
+ destination = "/local"
+ }
+ template {
+data = <<EOF
+# Agent Control and Service Port(Default : TCP 6100)
+net_tcp_listen_port={{ env "NOMAD_PORT_collector" }}
+
+# UDP Receive Port(Default : 6100)
+net_udp_listen_port={{ env "NOMAD_PORT_collector" }}
+
+# DB directory(Default : ./database)
+db_dir=./database
+
+# Log directory(Default : ./logs)
+log_dir=./logs
+EOF
+ destination = "local/scouter/server/conf/scouter.conf"
+ }
+ config {
+ class_path = "local/scouter/server/scouter-server-boot.jar"
+ class = "scouter.boot.Boot"
+ args = ["local/scouter/server/lib"]
+ }
+ service {
+ name = "scouter-collector"
+ tags = ["scouter"]
+
+ port = "collector"
+
+ check {
+ type = "tcp"
+ interval = "10s"
+ timeout = "2s"
+ port = "collector"
+ }
+ }
+ }
+ }
+}
+
+
+
system
타입으로 실행하나, 조건이 필요한 경우 Java가 있는 경우, 혹은 특정 노드에 대한 조건을 Constrain
으로 구성할 수 있음// nomad namespace apply -description "scouter" scouter
+
+variable "version" {
+ default = "2.15.0"
+}
+
+locals {
+ souter_release_url = "https://github.com/scouter-project/scouter/releases/download/v${var.version}/scouter-min-${var.version}.tar.gz"
+}
+
+job "scouter-host-agent" {
+ datacenters = ["dc1"]
+ // namespace = "scouter"
+
+ type = "system"
+
+ group "agent" {
+
+ task "agent" {
+ driver = "java"
+ resources {
+ cpu = 100
+ memory = 128
+ }
+ artifact {
+ source = local.souter_release_url
+ destination = "/local"
+ }
+ template {
+data = <<EOF
+obj_name=${node.unique.name}
+{{ range service "scouter-collector" }}
+net_collector_ip={{ .Address }}
+net_collector_udp_port={{ .Port }}
+net_collector_tcp_port={{ .Port }}
+{{ end }}
+#cpu_warning_pct=80
+#cpu_fatal_pct=85
+#cpu_check_period_ms=60000
+#cpu_fatal_history=3
+#cpu_alert_interval_ms=300000
+#disk_warning_pct=88
+#disk_fatal_pct=92
+EOF
+ destination = "local/scouter/agent.host/conf/scouter.conf"
+ }
+ config {
+ class_path = "local/scouter/agent.host/scouter.host.jar"
+ class = "scouter.boot.Boot"
+ args = ["local/lib"]
+ }
+ }
+ }
+}
+
+
-Dobj_name=Tomcat-${node.unique.name}-${NOMAD_PORT_http}
variable "tomcat_version" {
+ default = "10.0.14"
+}
+
+variable "scouter_version" {
+ default = "2.15.0"
+}
+
+locals {
+ tomcat_major_ver = split(".", var.tomcat_version)[0]
+ tomcat_download_url = "https://archive.apache.org/dist/tomcat/tomcat-${local.tomcat_major_ver}/v${var.tomcat_version}/bin/apache-tomcat-${var.tomcat_version}.tar.gz"
+ souter_release_url = "https://github.com/scouter-project/scouter/releases/download/v${var.scouter_version}/scouter-min-${var.scouter_version}.tar.gz"
+ war_download_url = "https://tomcat.apache.org/tomcat-10.0-doc/appdev/sample/sample.war"
+}
+
+job "tomcat-scouter" {
+ datacenters = ["dc1"]
+ // namespace = "scouter"
+
+ type = "service"
+
+ group "tomcat" {
+ count = 1
+
+ scaling {
+ enabled = true
+ min = 1
+ max = 3
+ }
+
+ task "tomcat" {
+ driver = "raw_exec"
+ resources {
+ network {
+ port "http" {}
+ port "stop" {}
+ port "jmx" {}
+ }
+ cpu = 500
+ memory = 512
+ }
+ env {
+ APP_VERSION = "0.1"
+ CATALINA_HOME = "${NOMAD_TASK_DIR}/apache-tomcat-${var.tomcat_version}"
+ CATALINA_OPTS = "-Dport.http=$NOMAD_PORT_http -Dport.stop=$NOMAD_PORT_stop -Ddefault.context=$NOMAD_TASK_DIR -Xms256m -Xmx512m -javaagent:local/scouter/agent.java/scouter.agent.jar -Dscouter.config=local/conf/scouter.conf -Dobj_name=Tomcat-${node.unique.name}-${NOMAD_PORT_http}"
+ JAVA_HOME = "/usr/lib/jvm/java-11-openjdk-amd64"
+ }
+ artifact {
+ source = local.tomcat_download_url
+ destination = "/local"
+ }
+ artifact {
+ source = local.souter_release_url
+ destination = "/local"
+ }
+ artifact {
+ source = local.war_download_url
+ destination = "/local/webapps"
+ }
+ template {
+data = <<EOF
+<?xml version="1.0" encoding="UTF-8"?>
+<Server port="${port.stop}" shutdown="SHUTDOWN">
+ <Listener className="org.apache.catalina.startup.VersionLoggerListener"/>
+ <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on"/>
+ <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener"/>
+ <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener"/>
+ <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener"/>
+ <GlobalNamingResources>
+ <Resource name="UserDatabase" auth="Container" type="org.apache.catalina.UserDatabase" description="User database that can be updated and saved" factory="org.apache.catalina.users.MemoryUserDatabaseFactory" pathname="conf/tomcat-users.xml"/>
+ </GlobalNamingResources>
+ <Service name="Catalina">
+ <Connector port="${port.http}" protocol="HTTP/1.1" connectionTimeout="20000"/>
+ <Engine name="Catalina" defaultHost="localhost">
+ <Realm className="org.apache.catalina.realm.LockOutRealm">
+ <Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/>
+ </Realm>
+ <Host name="localhost" appBase="${default.context}/webapps/" unpackWARs="true" autoDeploy="true">
+ <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" prefix="localhost_access_log" suffix=".txt" pattern="%h %l %u %t "%r" %s %b"/>
+ </Host>
+ </Engine>
+ </Service>
+</Server>
+EOF
+ destination = "local/conf/server.xml"
+ }
+ template {
+data = <<EOF
+{{ range service "scouter-collector" }}
+net_collector_ip={{ .Address }}
+net_collector_udp_port={{ .Port }}
+net_collector_tcp_port={{ .Port }}
+{{ end }}
+#hook_method_patterns=sample.mybiz.*Biz.*,sample.service.*Service.*
+#trace_http_client_ip_header_key=X-Forwarded-For
+#profile_spring_controller_method_parameter_enabled=false
+#hook_exception_class_patterns=my.exception.TypedException
+#profile_fullstack_hooked_exception_enabled=true
+#hook_exception_handler_method_patterns=my.AbstractAPIController.fallbackHandler,my.ApiExceptionLoggingFilter.handleNotFoundErrorResponse
+#hook_exception_hanlder_exclude_class_patterns=exception.BizException
+EOF
+ destination = "local/conf/scouter.conf"
+ }
+ config {
+ command = "${CATALINA_HOME}/bin/catalina.sh"
+ args = ["run", "-config", "$NOMAD_TASK_DIR/conf/server.xml"]
+ }
+ service {
+ name = "tomcat-scouter"
+ tags = ["tomcat"]
+
+ port = "http"
+
+ check {
+ type = "tcp"
+ interval = "10s"
+ timeout = "2s"
+ port = "http"
+ }
+ }
+ service {
+ name = "tomcat-scouter"
+ tags = ["jmx"]
+ port = "jmx"
+ }
+ }
+ }
+}
+
+
HashiCorp 공식 Service Mesh Test App
https://learn.hashicorp.com/tutorials/nomad/consul-service-mesh
job "countdash" {
+ region = "global"
+ datacenters = ["dc1"]
+ # namespace = "mesh"
+
+ group "api" {
+ network {
+ mode = "bridge"
+ port "api" {
+ to = 9001 # static 설정이 없으므로 컨테이너의 앱 9001과 노출되는 임의의 포트와 맵핑
+ }
+ }
+
+ service {
+ name = "count-api"
+ port = "api" # 임의의 포트 할당을 network port id로 지정
+
+ connect {
+ sidecar_service {}
+ }
+ }
+
+ task "web" {
+ driver = "docker"
+ config {
+ image = "hashicorpnomad/counter-api:v1"
+ ports = ["api"]
+ }
+ }
+ }
+
+ group "dashboard" {
+ network {
+ mode = "bridge"
+ port "http" {
+ static = 9002 # 컨테이너 앱 9002와 외부노출되는 사용자 지정 static port를 맵핑
+ to = 9002
+ }
+ }
+
+ service {
+ name = "count-dashboard"
+ port = "http" # 할당된 포트를 network port id로 지정
+
+ connect {
+ sidecar_service {
+ proxy {
+ upstreams {
+ destination_name = "count-api"
+ local_bind_port = 8080 # backend인 count-api의 실제 port와는 별개로 frontend가 호출할 port 지정
+ }
+ }
+ }
+ }
+ }
+
+ task "dashboard" {
+ driver = "docker"
+ env {
+ COUNTING_SERVICE_URL = "http://${NOMAD_UPSTREAM_ADDR_count_api}"
+ }
+ config {
+ image = "hashicorpnomad/counter-dashboard:v1"
+ }
+ }
+
+ scaling {
+ enabled = true
+ min = 1
+ max = 10
+ }
+ }
+}
+
job "01_tomcat-sidecar" {
+ datacenters = ["dc1"]
+
+ #ingress용도의 group
+ group "ingress-tomcat" {
+ network {
+ mode = "bridge"
+ port "inbound" {
+ static = 8080
+ to = 8080
+ }
+ }
+
+ service {
+ name = "tomcat-ingress"
+ port = "8080"
+
+ #여기서부터 sidecar ingress
+ connect {
+ gateway {
+ ingress {
+ listener {
+ port = 8080
+ protocol = "tcp"
+ service {
+ #아래 tomcat group에 service를 호출함
+ name = "backend"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ group "tomcat" {
+ network {
+ mode = "bridge"
+ }
+
+ service {
+ name = "backend"
+ port = "8080"
+ connect {
+ sidecar_service {}
+ }
+ }
+
+ task "tomcat" {
+ driver = "docker"
+ config {
+ image = "tomcat"
+ }
+ }
+ }
+}
+
+
+
variables {
+ tomcat_download_url = "https://archive.apache.org/dist/tomcat/tomcat-10/v10.0.10/bin/apache-tomcat-10.0.10.tar.gz"
+ // https://tomcat.apache.org/tomcat-10.0-doc/appdev/sample/
+ war_download_url = "https://tomcat.apache.org/tomcat-10.0-doc/appdev/sample/sample.war"
+}
+
+job "tomcat-10" {
+ datacenters = ["dc1"]
+ # namespace = "legacy"
+
+ type = "service"
+
+ group "tomcat" {
+ count = 1
+
+ scaling {
+ enabled = true
+ min = 1
+ max = 3
+ }
+
+ task "tomcat" {
+ driver = "raw_exec"
+ resources {
+ network {
+ port "http" {}
+ port "stop" {}
+ }
+ cpu = 500
+ memory = 512
+ }
+ env {
+ APP_VERSION = "0.1"
+ CATALINA_HOME = "${NOMAD_TASK_DIR}/apache-tomcat-10.0.10"
+ CATALINA_OPTS = "-Dport.http=$NOMAD_PORT_http -Dport.stop=$NOMAD_PORT_stop -Ddefault.context=$NOMAD_TASK_DIR -Xms256m -Xmx512m"
+ JAVA_HOME = "/usr/lib/jvm/java-11-openjdk-amd64"
+ }
+ template {
+data = <<EOF
+<?xml version="1.0" encoding="UTF-8"?>
+<Server port="${port.stop}" shutdown="SHUTDOWN">
+ <Listener className="org.apache.catalina.startup.VersionLoggerListener"/>
+ <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on"/>
+ <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener"/>
+ <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener"/>
+ <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener"/>
+ <GlobalNamingResources>
+ <Resource name="UserDatabase" auth="Container" type="org.apache.catalina.UserDatabase" description="User database that can be updated and saved" factory="org.apache.catalina.users.MemoryUserDatabaseFactory" pathname="conf/tomcat-users.xml"/>
+ </GlobalNamingResources>
+ <Service name="Catalina">
+ <Connector port="${port.http}" protocol="HTTP/1.1" connectionTimeout="20000"/>
+ <Engine name="Catalina" defaultHost="localhost">
+ <Realm className="org.apache.catalina.realm.LockOutRealm">
+ <Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/>
+ </Realm>
+ <Host name="localhost" appBase="${default.context}/webapps/" unpackWARs="true" autoDeploy="true">
+ <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" prefix="localhost_access_log" suffix=".txt" pattern="%h %l %u %t "%r" %s %b"/>
+ </Host>
+ </Engine>
+ </Service>
+</Server>
+EOF
+ destination = "local/conf/server.xml"
+ }
+ artifact {
+ source = var.tomcat_download_url
+ destination = "/local"
+ }
+ artifact {
+ source = var.war_download_url
+ destination = "/local/webapps"
+ }
+ config {
+ command = "${CATALINA_HOME}/bin/catalina.sh"
+ args = ["run", "-config", "$NOMAD_TASK_DIR/conf/server.xml"]
+ }
+ service {
+ name = "legacy-tomcat"
+ tags = ["tomcat"]
+ provider = "nomad"
+
+ port = "http"
+
+ check {
+ type = "tcp"
+ interval = "10s"
+ timeout = "2s"
+ port = "http"
+ }
+ }
+ }
+ }
+}
+
+
variables {
+ tomcat_download_url = "https://dlcdn.apache.org/tomcat/tomcat-10/v10.1.18/bin/apache-tomcat-10.1.18.zip"
+ war_download_url = "https://tomcat.apache.org/tomcat-10.1-doc/appdev/sample/sample.war"
+}
+
+job "tomcat-10" {
+ datacenters = ["dc1"]
+ # namespace = "legacy"
+
+ type = "service"
+
+ group "tomcat" {
+ count = 1
+
+ scaling {
+ enabled = true
+ min = 1
+ max = 3
+ }
+
+ network {
+ port "http" {}
+ port "stop" {}
+ }
+
+ task "tomcat" {
+ driver = "raw_exec"
+ kill_signal = "SIGQUIT"
+ resources {
+ cpu = 500
+ memory = 512
+ }
+ env {
+ APP_VERSION = "0.1"
+ CATALINA_HOME = "${NOMAD_TASK_DIR}/apache-tomcat-10.1.18"
+ CATALINA_OPTS = "-Xrs -Dport.http=${NOMAD_PORT_http} -Dport.stop=${NOMAD_PORT_stop} -Ddefault.context=${NOMAD_TASK_DIR} -Xms256m -Xmx512m"
+ JAVA_HOME = "C:/Java/jdk-11"
+ }
+ template {
+data = <<EOF
+<?xml version="1.0" encoding="UTF-8"?>
+<Server port="${port.stop}" shutdown="SHUTDOWN">
+ <Listener className="org.apache.catalina.startup.VersionLoggerListener"/>
+ <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on"/>
+ <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener"/>
+ <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener"/>
+ <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener"/>
+ <GlobalNamingResources>
+ <Resource name="UserDatabase" auth="Container" type="org.apache.catalina.UserDatabase" description="User database that can be updated and saved" factory="org.apache.catalina.users.MemoryUserDatabaseFactory" pathname="conf/tomcat-users.xml"/>
+ </GlobalNamingResources>
+ <Service name="Catalina">
+ <Connector port="${port.http}" protocol="HTTP/1.1" connectionTimeout="20000"/>
+ <Engine name="Catalina" defaultHost="localhost">
+ <Realm className="org.apache.catalina.realm.LockOutRealm">
+ <Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/>
+ </Realm>
+ <Host name="localhost" appBase="${default.context}/webapps/" unpackWARs="true" autoDeploy="true">
+ <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" prefix="localhost_access_log" suffix=".txt" pattern="%h %l %u %t "%r" %s %b"/>
+ </Host>
+ </Engine>
+ </Service>
+</Server>
+EOF
+ destination = "local/conf/server.xml"
+ }
+ artifact {
+ source = var.tomcat_download_url
+ destination = "local"
+ }
+ artifact {
+ source = var.war_download_url
+ destination = "local/webapps"
+ }
+ config {
+ command = "${CATALINA_HOME}/bin/catalina.bat"
+ args = ["run", "-config", "%NOMAD_TASK_DIR%/conf/server.xml"]
+ }
+ service {
+ name = "legacy-tomcat"
+ provider = "nomad"
+ tags = ["tomcat"]
+
+ port = "http"
+
+ check {
+ type = "tcp"
+ interval = "10s"
+ timeout = "2s"
+ port = "http"
+ }
+ }
+ }
+ }
+}
+
"-Xrs" Java Option on Windows
Windows에서는 -Xrs
옵션을 자바 옵션에 추가하지 않으면, 종료시 Thread Dump만 발생하고 프로세스가 종료되지 않아 Nomad Job이 갱신되거나 새로운 배포를 수행하여도 기존 프로세스 종료가 안되어 신규 task가 실행되지 않는 현상이 있다.
Consul과 함께 구성된 경우 nginx에 동적으로 backend 구성
job "nginx" {
+ datacenters = ["dc1"]
+ # namespace = "legacy"
+
+ group "nginx" {
+ count = 1
+
+ network {
+ port "http" {
+ static = 28080
+ }
+ }
+
+ service {
+ name = "nginx"
+ port = "http"
+ }
+
+ task "nginx" {
+ driver = "docker"
+
+ config {
+ image = "nginx"
+ ports = ["http"]
+ volumes = [
+ "local:/etc/nginx/conf.d",
+ ]
+ }
+
+ template {
+ data = <<EOF
+upstream backend {
+{{ range service "legacy-tomcat" }}
+ server {{ .Address }}:{{ .Port }}; # Tomcat
+{{ else }}server 127.0.0.1:65535; # force a 502
+{{ end }}
+}
+
+server {
+ listen {{ env "NOMAD_PORT_http" }};
+
+ location /sample {
+ proxy_pass http://backend;
+ }
+
+ location /status {
+ stub_status on;
+ }
+}
+EOF
+
+ destination = "local/load-balancer.conf"
+ change_mode = "signal"
+ change_signal = "SIGHUP"
+ }
+ }
+ }
+}
+
팁
nomad의 배포 방법 중 canary와 rolling update 관련된 내용입니다.
...
+ #canary update - 새로운 버전의 task를 canary 변수의 수만큼 기동시키고 상황에 맞게 확인 후 배포
+ group "canary" {
+ count = 5
+
+ update {
+ max_parallel = 1
+ canary = 1
+ min_healthy_time = "30s"
+ healthy_deadline = "10m"
+ #배포 실패시 자동으로 전 버전으로 돌아감(배포 중이던 task 제거됨)
+ auto_revert = true
+ #task가 기동되어도 자동으로 다음 버전으로 넘어가지 않음(배포 전 버전 task 제거되지않음)
+ auto_promote = false
+ }
+ }
+ #rolling update - 기동 중이던 task를 하나씩(max_parallel만큼) 신규 task로 변환하면서 배포
+ group "api-server" {
+ count = 6
+
+ update {
+ max_parallel = 2
+ min_healthy_time = "30s"
+ healthy_deadline = "10m"
+ }
+ }
+ #배포 시 service에 canary로 배포된 task에만 붙일 수 있는 tag 설정
+ service {
+ port = "http"
+ name = "canary-deployments"
+
+ tags = [
+ "live"
+ ]
+
+ canary_tags = [
+ "canary"
+ ]
+}
+...
+
Consul의 KV에 값을 저장하고 비교하여 task batch를 수행하는 예제
curl -X GET http://127.0.0.1:8500/v1/kv/docmoa/commit_date | jq -r '.[0].Value | @base64d'
+
key
를 사용하는 경우{{ key "docmoa/commit_date" }}
+
job "gs-mac-docmoa-build" {
+ datacenters = ["home"]
+ type = "batch"
+
+ periodic {
+ cron = "0 */5 * * * * *"
+ prohibit_overlap = true
+ time_zone = "Asia/Seoul"
+ }
+
+ constraint {
+ attribute = "${attr.unique.consul.name}"
+ value = "my-macbook-air"
+ }
+
+ group "docmoa-build" {
+ count = 1
+
+ task "git-pull" {
+ driver = "raw_exec"
+ template {
+ data = <<EOH
+#!/bin/sh
+cd /Users/gslee/workspaces/docs
+git pull origin main
+ EOH
+
+ destination = "git.sh"
+ }
+ config {
+ command = "git.sh"
+ }
+ lifecycle {
+ hook = "prestart"
+ sidecar = false
+ }
+ }
+ task "build" {
+ driver = "raw_exec"
+ template {
+ data = <<EOH
+#!/bin/sh
+LAST_COMMIT_DATE=$(curl https://api.github.com/repos/docmoa/docs/branches/main | jq -r '.commit.commit.committer.date')
+#STORE_COMMIT_DATE=$(curl -X GET http://127.0.0.1:8500/v1/kv/docmoa/commit_date | jq -r '.[0].Value | @base64d')
+STORE_COMMIT_DATE={{ key "docmoa/commit_date" }}
+echo "LAST_COMMIT_DATE = $LAST_COMMIT_DATE"
+echo "STORE_COMMIT_DATE = $STORE_COMMIT_DATE"
+if [ $LAST_COMMIT_DATE != $STORE_COMMIT_DATE ]; then
+ echo "do deploy"
+ # something todo...
+ # update new value
+ curl -X PUT --data $LAST_COMMIT_DATE http://127.0.0.1:8500/v1/kv/docmoa/commit_date
+else
+ echo "no change"
+fi
+ EOH
+
+ destination = "build.sh"
+ }
+ config {
+ command = "build.sh"
+ }
+ resources {
+ cpu = 1000
+ memory = 256
+ }
+ }
+ }
+}
+
job "logs" {
+ datacenters = ["dc1"]
+
+ constraint {
+ attribute = "${attr.kernel.name}"
+ value = "linux"
+ }
+
+ update {
+ stagger = "10s"
+ max_parallel = 1
+ }
+
+ group "elk" {
+ count = 1
+
+ restart {
+ attempts = 2
+ interval = "1m"
+ delay = "15s"
+ mode = "delay"
+ }
+ network {
+ port "elk" {
+ to = 9200
+ static = 9200
+ }
+ port "kibana" {
+ to = 5601
+ }
+ port "logstash" {
+ to = 5000
+ }
+ }
+
+ task "elasticsearch" {
+ driver = "docker"
+
+ vault {
+ policies = ["admin"]
+ change_mode = "signal"
+ change_signal = "SIGINT"
+ }
+
+ config {
+ image = "elasticsearch:7.16.2"
+ ports = ["elk"]
+ volumes = [
+ "local/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml",
+ ]
+ }
+ template {
+ data = <<EOF
+cluster.name: "my-cluster"
+network.host: 0.0.0.0
+discovery.type: single-node
+discovery.seed_hosts: ["127.0.0.1"]
+xpack.security.enabled: true
+xpack.license.self_generated.type: trial
+xpack.monitoring.collection.enabled: true
+EOF
+ destination = "local/elasticsearch.yml"
+ change_mode = "signal"
+ change_signal = "SIGHUP"
+ }
+ template {
+ data = <<EOH
+ELASTIC_PASSWORD="{{with secret "secret/elastic"}}{{.Data.passwd}}{{end}}"
+EOH
+
+ destination = "secrets/file.env"
+ env = true
+}
+
+ service {
+ name = "${TASKGROUP}-elasticsearch"
+ port = "elk"
+ }
+
+ resources {
+ cpu = 500
+ memory = 1048
+ }
+ }
+
+ task "kibana" {
+ driver = "docker"
+
+ vault {
+ policies = ["admin"]
+ change_mode = "signal"
+ change_signal = "SIGINT"
+ }
+
+ config {
+ image = "kibana:7.16.2"
+ ports = ["kibana"]
+ volumes = [
+ "local/kibana.yml:/usr/share/kibana/config/kibana.yml"
+ ]
+ }
+ template {
+ data = <<EOF
+#
+# ** THIS IS AN AUTO-GENERATED FILE **
+#
+
+# Default Kibana configuration for docker target
+server.host: "0.0.0.0"
+server.shutdownTimeout: "5s"
+elasticsearch.hosts: [ "http://{{ env "NOMAD_IP_elk" }}:{{ env "NOMAD_PORT_elk" }}" ]
+elasticsearch.username: elastic
+{{ with secret "secret/elastic" }}
+elasticsearch.password: {{.Data.passwd}}
+{{ end }}
+
+EOF
+
+ destination = "local/kibana.yml"
+ change_mode = "signal"
+ change_signal = "SIGHUP"
+ }
+
+ service {
+ name = "${TASKGROUP}-kibana"
+ port = "kibana"
+ check {
+ type = "http"
+ path = "/"
+ interval = "10s"
+ timeout = "2s"
+ }
+ }
+
+ resources {
+ cpu = 500
+ memory = 1200
+ }
+ }
+
+ task "logstash" {
+ driver = "docker"
+
+ config {
+ image = "logstash:7.16.2"
+ ports = ["logstash"]
+ volumes = [
+ "local/logstash.yml:/usr/share/logstash/config/logstash.yml"
+ ]
+ }
+ template {
+ data = <<EOF
+http.host: "0.0.0.0"
+xpack.monitoring.elasticsearch.hosts: [ "http://{{ env "NOMAD_IP_elk" }}:{{ env "NOMAD_PORT_elk" }}" ]
+EOF
+
+ destination = "local/logstash.yml"
+ change_mode = "signal"
+ change_signal = "SIGHUP"
+ }
+
+ service {
+ name = "${TASKGROUP}-logstash"
+ port = "logstash"
+ }
+
+ resources {
+ cpu = 200
+ memory = 1024
+ }
+ }
+ }
+}
+
Terraform
terraform test
관련 실제 존재하지 않는 자원에 대해 destroy 를 수행하려는 오류에 대한 개선v202312-1 (745)
)terraform plan -out <FILE>
또는 API 호출 시 save-plan
을 활용하여 plan 정보를 저장 후 추후 apply 시 활용하도록 지원inactive
인 Workspace 에 대해 자동 destroy 수행 지원Operation failed: failed packing filesystem: illegal slug error: invalid symlink
에러 발생하는 부분에 대해 2024 1월 release 에서 patch 예정us-east-1
로 설정invalid new value for .skip_destroy: was cty.False, but now null
오류 개선Vault
Consul
1.17.1
상세 Release Note
소스코드 Go 1.20.12 기반으로 변경 하여
CVE-2023-45283, CVE-2023-45284, CVE-2023-39326, CVE-2023-45285 관련 이슈 해결
ACL 관련 nomad client templated policy와 api-gateway templated policy 지원
peering exported-services
명령어 추가하여 peer cluster 에게 export 한 service 목록 확인 지원
Consul Telemetry Collector 의 stats_flush_interval
에 대한 기본값을 60초로 지정
Wan Federation 관련 Secondary DC 에 대해 Replication 을 위한 불필요한 쓰기 작업을 방지하기 위해 hash 적용
Nomad
nomad job stop
등의 수동 개입이 필요했던 부분에 대해 자동으로 reschedule 방지 되도록 지원v202401-1 (751)
)consolidated_services_enabled
설정 지원 종료/
가 포함되는 것 허용상세 Release Note
Known Issue: 1.17.2 및 1.16.5 에서 Terminating Gatteway 가 TLS SAN 검증을 엄격하게 수행함으로 인해 Service Mesh 외부 Upstream 서비스 연결 방지
sameness-group 기반 failover 수행 시 해당 group이 속해있는 partition 이 아닌 기본 partition 으로 질의 하는 오류 개선
Dynamic Secrets for Waypoint with Vault
대상 OS
Debian 8, 9
Ubuntu 14.04, 16.04
Amazon Linux 2014.03, 2014.09, 2015.03, 2015.09, 2016.03, 2016.09, 2017.03, 2017.09, 2018.03
대상 Postgres
상세 CHANGELOG
consul connect: vault connect CA test 에 대해 제한된 권한의 token 사용하도록 수정
consul connect 관련 Mesh Gateway 의 Failover peering 에 대한 오류 개선
consul connect 관련 Vault 1.11+ 를 CA Provider 로 사용 시 발생하는 Intermediate CA 관련 오류 개선
Cluster Peering 기반 Service Failover 포함 Mesh Traffic Management 기능 개선
Windows 에 대한 Service Mesh 지원
AWS Lambda 에 대한 Service Mesh 지원
Terraform Cloud Adds ‘Projects’ to Organize Workspaces at Scale
v202301-2
)v202302-1
) 을 기점으로 구버전 OS 및 Postrgres DB 에 대한 지원 종료 대상 OS
Debian 8, 9
Ubuntu 14.04, 16.04
Amazon Linux 2014.03, 2014.09, 2015.03, 2015.09, 2016.03, 2016.09, 2017.03, 2017.09, 2018.03
대상 Postgres
aws_auditmanager resource
추가kms_key_arn
관련 오류 개선connection type
이 변경되는 오류 개선azure_site_recovery_replication_recovery_plan
customer managed key
및 public_network_access_enabled
설정 지원node_public_ip_tags
설정 지원google_iam_access_boundary_policy
, google_tags_location_tag_bindings
google_spanner_database_iam_member
및 google_spanner_instance_iam_member
설정 지원default_follows_latest_issuer
설정 추가retry_join_as_non_voter
설정 추가proxy-defaults
, service-default
) 수정envoy-ready-bind-port
및 envoy-ready-bind-address
설정 지원Writing Terraform for unsupported resources
v202302-1 (681)
)agent run pipeline mode
사용 시 실행 계획이 무기한 Queue 에 대기하는 문제 발생. 관련하여 tfe-task-worker
를 통해 [ERROR] core: Unexpected HTTP response code: method=POST url=https://terraform.example.com/api/agent/register status=404
라는 Error Log 출력되며 2023년 3월 Release (v202303-1
) 에서 해결 예정manage-workspaces
권한 부여 시, read-workspaces
도 부여되는 문제 발생. 더불어 manage-workspaces
에 대한 권한 제거 시 read-workspaces
권한은 제거 되지 않고 부여된 채로 유지 되는 문제 발생. 이후 출시 예정인 Release 에서 해결 예정skip_destroy
를 Null 값 처리 시 발생하는 Provider produced inconsitent final plan
오류 개선azurerm_mobile_network
및 azurerm_sentinel_alert_rule
google_data_catalog
, google_scc_mute_config
, google_workstations_workstation_config
EnvoyExtensions
설정을 통해 lua 및 lambda 등에 대한 service mesh 연동 지원consul troubleshoot
명령어 지원을 통해 서비스 간 통신 오류에 대한 진단 지원Dynamic provider credentials now generally available for Terraform Cloud
동적인증처리
를 지원합니다.invalid plan
오류 개선v202303-1 (688)
)tags
설정 지원snapshot_identifier
와 global_cluster_identifier
설정에 의한 잘못된 복원 수행 개선geo_backup_key_vault_key_id
와 geo_backup_user_assigned_identity_id
설정 지원allow_forwarded_traffic
, allow_gateway_transit
, use_remote_gateways
에 대한 기본값 설정hub_routing_preference
설정 지원auth_v2
, token_store_enabled
, ip_restriction
, scp_ip_restriction
관련 오류 개선auth_v2
, token_store_enabled
, ip_restriction
, scp_ip_restriction
관련 오류 개선auth_v2
, token_store_enabled
, ip_restriction
, scp_ip_restriction
관련 오류 개선auth_v2
, token_store_enabled
, ip_restriction
, scp_ip_restriction
관련 오류 개선is_case_insensitive
와 default_collation
설정 지원scratch_disk.size
와 local_nvme_ssd_block
설정 지원managed.dns_authorizations
관련 오류 개선enforce_on_key_name
관련 설정 오류 개선VAULT_AUTH_CONFIG_GITHUB_TOKEN
설정 지원CKR_FUNCTION_FAILED
오류 발생 시 PKCS#11 HSM 에 대한 재연결 시도 개선consul token update
명령어 수행 시 -append-policy-id
, -append-policy-name
, -append-role-id
, -append-service-identity
, -append-node-identity
매개변수 설정 지원HTTPRoute
관련 오류 개선namespace status
, quota status
, server members
명령어에 대해 -json
과 -t
매개변수 설정 지원Vault Secrets Operator: A new method for Kubernetes integration
CLI
create_before_destroy
참조 오류 등 개선v202304-1 (692)
)Provider
launch_template
설정 추가role_last_used
설정 추가compatible_runtimes = python 3.10
설정 추가hosting_environment_id
설정 추가hosting_environment_id
설정 추가query_string
길이 제약 제거dynamic_criteria.0.ignore_data_before
미설정시 발생하는 오류 개선point_in_time_restore_time_in_utc
관련 오류 개선location
설정 required 로 변경 (기존: optional)\inspect_job.actions.job_notification_emails
, inspect_job.actions.deidentify
, triggers.manual
그리고 inspect_job.storage_config.hybrid_options
설정 추가weekly_schedule
설정 optional 로 변경USE_ORIGIN_HEADERS
사용시 발생하는 TTL 관련 오류 개선VAULT_AUTH_CONFIG_GITHUB_TOKEN
설정 지원CKR_FUNCTION_FAILED
오류 발생 시 PKCS#11 HSM 에 대한 재연결 시도 개선unmarshal
되는 오류 개선Streaming not supported
발생하는 오류 개선Terraform Cloud updates plans with an enhanced Free tier and more flexibility
Terraform Cloud adds Vault-backed dynamic credentials
terraform plan
시 null string 또는 잘못 정의된 map 으로 인한 오류 개선terraform plan
시 구버전 TFE 에서 plan 두번 되는 이슈 개선forces replacement
설정된 자원 관련 오류 개선v202305-1 (703)
)TFC_VAULT_ENCODED_CACERT
환경변수 사용 지원createMode
를 nil 대신 default 로 대체error performing token check: no lease entry found for token that ought to have one, possible eventual consistency issue
오류 개선go/scanner
), CVE-2023-24538(html/template
), CVE-2023-24534(net/textproto
), CVE-2023-24536(mime/multipart
)1.22.11
, 1.23.8
, 1.24.6
, 1.25.4
MaxEjectionPercent
과 BaseEjectionTime
설정 지원check
Block 추가, 최소 1개 이상의 assert
구문을 요구하며 assert
구문 내 1개 혹은 여러 개의 condition
과 error_message
를 지정하여 resource
또는 data
자원에 대한 검증 지원import
Block 추가, 기존 terraform import 명령어 기반 import 작업을 hcl code 로 작성하여 보다 손쉽게 import 작업 수행하도록 지원v202306-1 (713)
)v202308-1
부터 TFE 구동을 위한 Container 구성이 terraform-enterprise
라는 단일 Container 로 통합 (terraform plan
또는 terraform apply
는 기존 방식과 동일하게 수행 때마다 agent container 생성)v202308-1
부터 Docker 19.03 지원 종료 예정ap-east-1
region 에서 code_signing_config_arn
설정 지원forbidden_account_ids
처리 오류 개선InvalidParameterException: You cannot specify both rotation frequency and schedule expression together
오류 개선InvalidParameter: PrivateDnsOnlyForInboundResolverEndpoint not supported for this service
오류 개선public_network_access_enabled
설정 지원public_network_access_enabled
설정 지원allowed_origin_patterns
설정 지원cors
설정 시 allowed_origins
에 대한 설정 항목 수 오류 개선cors
설정 시 allowed_origins
에 대한 설정 항목 수 오류 개선google_compute_forwarding_rule
설정 내 no_automate_dns_zone
추가google_compute_disk_async_replication
정식 지원google_compute_disk
설정 내 async_primary_disk
추가google_compute_region_disk
설정 내 async_primary_disk
추가google_network_services_edge_cache_keyset
의 기본 timeout 값을 90 분으로 설정/v1/health/connect/
와 /v1/health/ingress/
api 에 대해 적절한 권한 없는 token 사용 시 403 오류 출력consul services export
: peer cluster 또는 타 partition 에 서비스 노출을 위한 명령어 추가check
Block 관련 오류 개선v202307-1 (722)
)terraform-enterprise
라는 단일 Container 로 통합. terraform plan
또는 terraform apply
는 기존 방식과 동일하게 수행 때마다 agent container 생성 (적용시점: v202309-1
부터 적용)v202308-1
부터 적용)Manage Policy Overrides
에 대해 기본 부여된 정책 수정 (기존 Read -> List)/v1/health/connect/
와 /v1/health/ingress/
api 에 대해 적절한 권한 없는 token 사용 시 403 오류 출력consul services export
: peer cluster 또는 타 partition 에 서비스 노출을 위한 명령어 추가terraform_remote_state
를 읽어 오는 중 발생 가능한 오류에 대한 개선v202308-1 (725)
)terraform-enterprise
라는 단일 Container 로 통합. terraform plan
또는 terraform apply
는 기존 방식과 동일하게 수행 때마다 agent container 생성 (적용시점: v202309-1
부터 적용)dynamic provider credentials
에 대해 Workspace 내 Provider 당 설정 지원setting protocol: Invalid address to set
오류 수정tag propagation: timeout while waiting for state to become 'TRUE'
오류 개선consul members
명령어에 대해 -filter 설정 추가terraform init
수행 시 잘못된 경로로 모듈을 다운로드 하는 경우에 대한 오류 개선terraform remote state
관리 시 발생 가능한 state 간 호환성 불일치에 대한 이슈 방지v202309-1 (733)
)2020 Update 1.2 Patch 7
로 업그레이드 하여 해결step-up
추가 인증 이슈에 10월 Release 에서 개선 예정terraform test
기능 추가 - 작성한 terraform code 에 대해 .tftest.hcl code 를 작성하여 검증 지원AWS_ENDPOINT_URL_DYNAMODB
, AWS_ENDPOINT_URL_IAM
, AWS_ENDPOINT_URL_S3
, AWS_ENDPOINT_URL_STS
v202310-1 (741)
)consolidated_services_enabled
설정 여부에 따라 기존 replicated 설치 방식 사용 지원.2020 Update 1.2 Patch 7
로 업그레이드 하여 해결consolidated_services_enabled
설정 활성화하고 설치 시 발생하는 Object Storage Issue 는 v202311-1 에서 개선 예정file
사용 시 SIGHUP 에서 발생하는 log file 재열기 오류 개선HCP Boundary 출시 (Public Beta)
Hashicorp Developer Site 출시 (Public Beta)
Consul Service Mesh 에 대한 AWS Lambda 지원 (Public Beta)
CDKTF (Cloud Development Kit for Terraform) General Available
Terraform
CLI
8월 Release 출시 (v202208-3)
필수 Upgrade Version: Release Note 에서 * 표기된 Version 은 필수로 거쳐야 하는 Version (예: v202207-2, v202204-2)
VCS 기반 Workspace 생성 시 정의된 variable 에 대한 type 검증 및 오류 안내 기능 추가
Known Issue: TFE 내부 모듈 중 하나인 Postgres - ver 10 또는 11 에 대한 Migration 실패 오류 개선 (v202208-2)
Run Pipeline 에 대한 Metric 정보가 Prometheus 에 표시 되지 않는 이슈 개선
Module Registry Protocol endpoint 인 '/v1/modules/{namespace}/{name}/{provider}/versions' 에서 version 갯수가 많은 Module 처리시 발생하는 오류 개선
Provider
Vault
Consul
Nomad
Nomad: Nomad Variables and Service Discovery
9월 Release 출시 (v202209-2)
필수 Upgrade Version: Release Note 에서 * 표기된 Version 은 필수로 거쳐야 하는 Version (예: v202207-2, v202204-2)
TFE 엔진 내 Vault 에 대한 정책 수정하여 간헐적으로 TFE 구동 시 발생하는 403 오류 해결
TFE 엔진 내 Data Migration Logic 에 대해 Postgres 11 이상에서만 지원
TF Code 변경에 대한 Test 등을 지원하는 예측 계획 (Speculative Plans) 기능 지원
Hashiconf Global
Day 1: ZTS (Zero Trust Security) 와 Cloud Service Networking 을 메인 주제로 새로운 기능과 HCP 서비스에 대한 소개
Day 2: Infrastructure 및 Application 자동화 관련 제품군을 메인 주제로 새로운 기능 소개
Terraform Cloud 신규 기능 (현재 모두 Beta, Hashiconf Global 발표 참고)
Continous Validation: Terraform 으로 Provisioning 한 (Day 0) Resource 에 대한 수동 변화를 감지하는 Drift Detection 와 더불어 장기적 관리 및 유지보수 관점에서 필요한 사전 (Precondition) 및 사후 (Postcondition) 조건을 기반으로 Resource 의 상태를 점검하고 관리하는 기능
No-Code Provisioning: Terraform 에 대해 Code 작성과 같은 기본 지식 또는 Module 과 같은 고급 지식에 대한 이해 없이 최소한의 변수 정보 입력만으로 Terraform 기반의 Workspace 생성 부터 Resource Provisioning 까지 사용할 수 있게 지원하는 Self-Service 특화 기능
OPA (Open Policy Agent) 기반 정책 관리: Rego 정책 언어 기반 OPA 릴 지원하여 기존에 OPA 기반 표준 정책 수립한 사용자도 손쉽게 Terraform Cloud 에 Import 하여 정책 기반 Resource Provisioning 을 지원하는 기능
CLI
10월 Release 출시 (v202210-1)
필수 Upgrade Version: Release Note 에서 * 표기된 Version 은 필수로 거쳐야 하는 Version (예: v202207-2, v202204-2)
PostgresSQL 버전 10 지원종료: TFE 에 대해 External PostgresSQL 사용하는 경우 최소 버전 12 이상으로 Upgrade 필요
구버전 OS 지원종료: 2023년 2월 release (v202302-1) 을 기점으로 아래 OS 목록에 대해 지원 종료
Debian 8, 9
Ubuntu 14.04, 16.04
Amazon Linux 2014.03, 2014.09, 2015.03, 2015.09, 2016.03, 2016.09, 2017.03, 2017.09, 2018.03
Provider
Terraform Run Tasks in Public Registry
Terraform
대상 OS
Debian 8, 9
Ubuntu 14.04, 16.04
Amazon Linux 2014.03, 2014.09, 2015.03, 2015.09, 2016.03, 2016.09, 2017.03, 2017.09, 2018.03
대상 Postgres
Vault
Consul
Nomad
다양한 플랫폼에 대한 VM, 컨테이너 이미지 생성 자동화 도구
로컬 개발환경을 관리하는 프로비저닝 자동화 도구
클라우드, 온프레미스, 플랫폼 서비스의 리소스 프로비저닝과 자동화
서비스 디스커버리와 서비스 메시로 네트워크 자동화
인증/인가 기반으로 서버와 서비스에 대한 접근관리
민감 정보의 관리와 접근에 중앙화된 관리 서비스
애플리케이션 배포와 실행을 위한 오케스트레이터
단일 구성으로 컨테이너 환경에 애플리케이션 빌드 및 배포
BIN_DIR
로 지정된 /Users/my/Tools/bins/
는 PATH에 적용된 위치#!/bin/bash
+
+echo "<<<<<<< CHECK POINT HASHICORP RELEASE >>>>>>>"
+export RELEASE_URL="https://releases.hashicorp.com/"
+export DOWNLOAD_DIR="/tmp/hashistack-zip/"
+export BIN_DIR="/Users/my/Tools/bins/"
+
+if [ ! -d ${DOWNLOAD_DIR} ]; then
+ mkdir -p ${DOWNLOAD_DIR}
+fi
+
+cd ${DOWNLOAD_DIR}
+rm -rf ${DOWNLOAD_DIR}/*
+
+UPDATE_LIST=""
+
+for SOLUTION in "terraform" "consul" "vault" "nomad" "packer" "consul-terraform-sync" "waypoint" "boundary"; do
+ echo "Check - ${SOLUTION}"
+ TAG=$(curl -fsS https://api.github.com/repos/hashicorp/${SOLUTION}/releases \
+ | jq -re '.[] | select(.prerelease != true) | .tag_name' \
+ | sed 's/^v\(.*\)$/\1/g' \
+ | sort -V \
+ | tail -1)
+
+ export CURRENT_VERSION="0.0.0"
+ if [ -f "${BIN_DIR}/${SOLUTION}" ]; then
+ if [ ${SOLUTION} = "consul-terraform-sync" ]; then
+ CURRENT_VERSION=$(${SOLUTION} --version | awk '{print $2}' | head -1 | sed 's/v//' | sed 's/+ent//')
+ elif [ ${SOLUTION} = "boundary" ]; then
+ CURRENT_VERSION=$(${SOLUTION} version | head -4 | tail -1 | awk '{print $3}')
+ else
+ CURRENT_VERSION=$(${SOLUTION} version | awk '{print $2}' | head -1 | sed 's/v//')
+ fi
+ fi
+
+ if [ $TAG != $CURRENT_VERSION ]; then
+ if [ ${SOLUTION} = "consul-terraform-sync" ]; then
+ TAG=${TAG}+ent
+ fi
+ echo ">>>> ${SOLUTION} update ${CURRENT_VERSION} --> ${TAG}"
+ ZIP="${SOLUTION}_${TAG}_darwin_amd64.zip"
+ DOWNLOAD_URL="${RELEASE_URL}${SOLUTION}/${TAG}/${ZIP}"
+ wget -O "${DOWNLOAD_DIR}${ZIP}" ${DOWNLOAD_URL}
+ unzip -o ${DOWNLOAD_DIR}${ZIP} -d $BIN_DIR
+ rm -rf ${DOWNLOAD_DIR}${ZIP}
+ UPDATE_LIST="${UPDATE_LIST} ${SOLUTION}_${CURRENT_VERSION}\t-->>\t${SOLUTION}_${TAG}"
+ else
+ UPDATE_LIST="${UPDATE_LIST} ${SOLUTION}_${CURRENT_VERSION}"
+ fi
+done
+
+if [ "$(ls -A ${DOWNLOAD_DIR})" ]; then
+ mv ${DOWNLOAD_DIR}* /Users/gs/Tools/bins/
+fi
+
+echo -e "\n==== HASHISTACK VERSION ===="
+for list in $UPDATE_LIST
+do
+ echo -e $list
+done
+
+
Update at 31 Jul, 2019
Jenkins Pipeline 을 구성하기 위해 VM 환경에서 Jenkins와 관련 Echo System을 구성합니다. 각 Product의 버전은 문서를 작성하는 시점에서의 최신 버전을 위주로 다운로드 및 설치되었습니다. 구성 기반 환경 및 버전은 필요에 따라 변경 가능합니다.
Category | Name | Version |
---|---|---|
VM | VirtualBox | 6.0.10 |
OS | Red Hat Enterprise Linux | 8.0.0 |
JDK | Red Hat OpenJDK | 1.8.222 |
Jenkins | Jenkins rpm | 2.176.2 |
Jenkins를 실행 및 구성하기위한 OS와 JDK가 준비되었다는 가정 하에 진행합니다. 필요 JDK 버전 정보는 다음과 같습니다.
필요 JDK를 설치합니다.
$ subscription-manager repos --enable=rhel-8-for-x86_64-baseos-rpms --enable=rhel-8-for-x86_64-appstream-rpms
+
+### Java JDK 8 ###
+$ yum -y install java-1.8.0-openjdk-devel
+
+### Check JDK version ###
+$ java -version
+openjdk version "1.8.0_222"
+OpenJDK Runtime Environment (build 1.8.0_222-b10)
+OpenJDK 64-Bit Server VM (build 25.222-b10, mixed mode)
+
Red Hatsu/Fedora/CentOS 환경에서의 Jenkins 다운로드 및 실행은 다음의 과정을 수행합니다.
repository를 등록합니다.
$ sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo
+$ sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key
+
작성일 기준 LTS 버전인 2.176.2
버전을 설치합니다.
$ yum -y install jenkins
+
패키지로 설치된 Jenkins의 설정파일은 /etc/sysconfig/jenkins
에 있습니다. 해당 파일에서 실행시 활성화되는 포트 같은 설정을 변경할 수 있습니다.
## Type: integer(0:65535)
+## Default: 8080
+## ServiceRestart: jenkins
+#
+# Port Jenkins is listening on.
+# Set to -1 to disable
+#
+JENKINS_PORT="8080"
+
외부 접속을 위해 Jenkins에서 사용할 포트를 방화벽에서 열어줍니다.
$ firewall-cmd --permanent --add-port=8080/tcp
+$ firewall-cmd --reload
+
서비스를 부팅시 실행하도록 활성화하고 Jenkins를 시작합니다.
$ systemctl enable jenkins
+$ systemctl start jenkins
+
실행 후 브라우저로 접속하면 Jenkins가 준비중입니다. 준비가 끝나면 Unlock Jenkins
페이지가 나오고 /var/lib/jenkins/secrets/initialAdminPassword
의 값을 입력하는 과정을 설명합니다. 해당 파일에 있는 토큰 복사하여 붙여넣습니다.
이후 과정은 Install suggested plugins
를 클릭하여 기본 플러그인을 설치하여 진행합니다. 경우에 따라 Select plugins to install
을 선택하여 플러그인을 지정하여 설치할 수 있습니다.
플러그인 설치 과정을 선택하여 진행하면 Getting Started
화면으로 전환되어 플러그인 설치가 진행됩니다.
설치 후 기본 Admin User
를 생성하고, 접속 Url을 확인 후 설치과정을 종료합니다.
진행되는 실습에서는 일부 GitHub를 SCM으로 연동합니다. 원활한 진행을 위해 GitHub계정을 생성해주세요. 또는 별개의 Git 서버를 구축하여 사용할 수도 있습니다.
Jenkins는 간단히 테마와 회사 CI를 적용할 수 있는 플러그인이 제공됩니다.
Jenkins 관리
로 이동하여 플러그인 관리
를 클릭합니다.
설치 가능
탭을 클릭하고 상단의 검색에 theme
를 입력하면 Login Theme
와 Simple Theme
를 확인 할 수 있습니다. 둘 모두 설치합니다.
로그아웃을 하면 로그인 페이지가 변경된 것을 확인 할 수 있습니다.
기본 Jenkins 테마를 변경하기 위해서는 다음의 과정을 수행합니다.
Build your own theme with a company logo!
에서 색상과 로고를 업로드 합니다.
DOWNLOAD YOUR THEME!
버튼을 클릭하면 CSS파일이 다운됩니다.
Jenkins 관리
로 이동하여 시스템 설정
를 클릭합니다.
Theme
항목의 Theme elements
의 드롭다운 항목에서 Extra CSS
를 클릭하고 앞서 다운받은 CSS파일의 내용을 붙여넣고 설정을 저장하면 적용된 테마를 확인할 수 있습니다.
프로젝트는 Job의 일부 입니다. 즉, 모든 프로젝트가 Job이지만 모든 Job이 프로젝트는 아닙니다. Job의 구조는 다음과 같습니다.
FreeStyleProejct, MatrixProject, ExternalJob만 New job
에 표시됩니다.
Step 1에서는 stage
없이 기본 Pipeline을 실행하여 수행 테스트를 합니다.
Jenkins 로그인
좌측 새로운 Item
클릭
Enter an item name
에 Job 이름 설정 (e.g. 2.Jobs)
Pipeline
선택 후 OK
버튼 클릭
Pipeline
항목 오른 쪽 Try sample Pipelie...
클릭하여 Hello world
클릭 후 저장
node {
+ echo 'Hello World'
+}
+
좌측 Build now
클릭
좌측 Build History
의 최근 빌드된 항목(e.g. #1) 우측에 마우스를 가져가면 dropdown 버튼이 생깁니다. 해당 버튼을 클릭하여 Console Output
클릭
수행된 echo
동작 출력을 확인합니다.
Started by user GyuSeok.Lee
+Running in Durability level: MAX_SURVIVABILITY
+[Pipeline] Start of Pipeline
+[Pipeline] node
+Running on Jenkins in /var/lib/jenkins/workspace/2.Jobs
+[Pipeline] {
+[Pipeline] echo
+Hello World
+[Pipeline] }
+[Pipeline] // node
+[Pipeline] End of Pipeline
+Finished: SUCCESS
+
Step 2에서는 stage
를 구성하여 실행합니다.
기존 생성한 Job 클릭 (e.g. 02-02.Jobs)
좌측 구성
을 클릭하여 Pipeline
스크립트를수정합니다.
pipeline{
+ agent any
+ stages {
+ stage("Hello") {
+ steps {
+ echo 'Hello World'
+ }
+ }
+ }
+}
+
수정 후 좌측 Build Now
를 클릭하여 빌드 수행 후 결과를 확인합니다.
Step 1
에서의 결과와는 달리 Stage View
항목과 Pipeline stage가 수행된 결과를 확인할 수 있는 UI가 생성됩니다.
수행된 빌드의 Console Output
을 확인하면 앞서 Step 1
에서는 없던 stage 항목이 추가되어 수행됨을 확인 할 수 있습니다.
Started by user GyuSeok.Lee
+Running in Durability level: MAX_SURVIVABILITY
+[Pipeline] Start of Pipeline
+[Pipeline] node
+Running on Jenkins in /var/lib/jenkins/workspace/2.Jobs
+[Pipeline] {
+[Pipeline] stage
+[Pipeline] { (Hello)
+[Pipeline] echo
+Hello World
+[Pipeline] }
+[Pipeline] // stage
+[Pipeline] }
+[Pipeline] // node
+[Pipeline] End of Pipeline
+Finished: SUCCESS
+
Pipeline 내에서 사용되는 매개변수 정의를 확인해 봅니다. Pipeline 스크립트는 다음과 같습니다.
pipeline {
+ agent any
+ parameters {
+ string(name: 'Greeting', defaultValue: 'Hello', description: 'How should I greet the world?')
+ }
+ stages {
+ stage('Example') {
+ steps {
+ echo "${params.Greeting} World!"
+ }
+ }
+ }
+}
+
parameters
항목내에 매개변수의 데이터 유형(e.g. string)을 정의합니다. name
은 값을 담고있는 변수이고 defaultValue
의 값을 반환합니다. Pipeline에 정의된 parameters
는 params
내에 정의 되므로 ${params.매개변수이름}
과 같은 형태로 호출 됩니다.
저장 후 다시 구성
을 확인하면 이 빌드는 매개변수가 있습니다
가 활성화 되고 내부에 추가된 매개변수 항목을 확인 할 수 있습니다.
이렇게 저장된 Pipeline Job은 매개변수를 외부로부터 받을 수 있습니다. 따라서 좌측의 기존 Build Now
는 build with Parameters
로 변경되었고, 이를 클릭하면 Greeting을 정의할 수 있는 UI가 나타납니다. 해당 매개변수를 재정의 하여 빌드를 수행할 수 있습니다.
다중스텝을 위한 Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 02-04.MultiStep)
Pipeline에 다음과 같이 스크립트를 추가합니다.
pipeline {
+ agent any
+ stages {
+ stage('Build') {
+ steps {
+ sh 'echo "Hello World"'
+ sh '''
+ echo "Multiline shell steps works too"
+ ls -lah
+ '''
+ }
+ }
+ }
+}
+
'''
은 스크립트 정의 시 여러줄을 입력할 수 있도록 묶어주는 역할을 합니다. 해당 스크립트에서는 sh
로 구분된 스크립트 명령줄이 두번 수행됩니다.
실행되는 여러 스크립트의 수행을 stage
로 구분하기위해 기존 Pipeline 스크립트를 다음과 같이 수정합니다.
pipeline {
+ agent any
+ stages {
+ stage('Build-1') {
+ steps {
+ sh 'echo "Hello World"'
+ }
+ }
+ stage('Build-2') {
+ steps {
+ sh '''
+ echo "Multiline shell steps works too"
+ ls -lah
+ '''
+ }
+ }
+ }
+}
+
stage를 구분하였기 때문에 각 실행되는 sh
스크립트는 각 스테이지에서 한번씩 수행되며, 이는 빌드의 결과로 나타납니다.
Pipeline의 step을 추가하여 결과를 확인하는 과정을 설명합니다. 피보나치 수열을 수행하는 쉘 스크립트를 시간제한을 두어 수행하고 그 결과를 확인합니다.
Jenkins가 설치된 서버에 [피보나치 수열]([https://namu.wiki/w/피보나치 수열](https://namu.wiki/w/피보나치 수열))을 수행하는 스크립트를 작성합니다. Sleep이 있기 때문에 일정 시간 이상 소요 됩니다.
$ mkdir -p /var/jenkins_home/scripts
+$ cd /var/jenkins_home/scripts
+$ vi ./fibonacci.sh
+#!/bin/bash
+N=${1:-10}
+
+a=0
+b=1
+
+echo "The Fibonacci series is : "
+
+for (( i=0; i<N; i++ ))
+do
+ echo "$a"
+ sleep 2
+ fn=$((a + b))
+ a=$b
+ b=$fn
+done
+# End of for loop
+
+$ chown -R jenkins /var/jenkins_home/
+$ chmod +x /var/jenkins_home/scripts/fibonacci.sh
+
다중스텝을 위한 Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 02-05.AddingStep)
Pipeline에 다음과 같이 스크립트를 추가합니다.
pipeline {
+ agent any
+ stages {
+ stage('Deploy') {
+ steps {
+ timeout(time: 1, unit: 'MINUTES') {
+ sh '/var/jenkins_home/scripts/fibonacci.sh 5'
+ }
+ timeout(time: 1, unit: 'MINUTES') {
+ sh '/var/jenkins_home/scripts/fibonacci.sh 32'
+ }
+ }
+ }
+ }
+}
+
steps
에 스크립트를 timeout
이 감싸고 있으며, 각 스크립트의 제한시간은 1분입니다. 빌드를 수행하면 최종적으로는 aborted
, 즉 중단됨 상태가 되는데 그 이유는 빌드 기록에서 해당 빌드를 클릭하면 확인 가능합니다.
Build History
에서 최신 빌드를 클릭합니다.
좌측 Pipeline Steps
를 클릭하면 Pipeline 수행 스텝을 확인할 수 있습니다.
첫번째로 나타나는 /var/jenkins_home/scripts/fibonacci.sh 5
를 수행하는 Shell Script
의 콘솔창 버튼을 클릭하면 잘 수행되었음을 확인 할 수 있습니다.
두번째로 나타나는 /var/jenkins_home/scripts/fibonacci.sh 32
를 수행하는 Shell Script
의 콘솔창 버튼을 클릭하면 다음과 같이 중도에 프로세스를 중지한 것을 확인 할 수 있습니다.
+ /var/jenkins_home/scripts/fibonacci.sh 32
+The Fibonacci series is :
+0
+1
+1
+2
+3
+...
+317811
+514229
+Sending interrupt signal to process
+/var/jenkins_home/scripts/fibonacci.sh: line 16: 13543 Terminated sleep 2
+832040
+/var/lib/jenkins/workspace/02-05.AddingStep@tmp/durable-e44bb232/script.sh: line 1: 13109 Terminated /var/jenkins_home/scripts/fibonacci.sh 32
+script returned exit code 143
+
Pipeline이 수행되는 동작을 추적하는 과정을 확인합니다. 이를 이를 위한 Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 03-01.TrackingBuildState)
Pipeline에 다음과 같이 스크립트를 추가합니다.
pipeline {
+ agent any
+ stages {
+ stage('Deploy') {
+ steps {
+ timeout(time: 1, unit: 'MINUTES') {
+ sh 'for n in `seq 1 10`; do echo $n; sleep 1; done'
+ }
+ timeout(time: 1, unit: 'MINUTES') {
+ sh 'for n in `seq 1 50`; do echo $n; sleep 1; done'
+ }
+ }
+ }
+ }
+}
+
Build Now
를 클릭하여 빌드를 수행합니다. 그러면, 좌측의 Build History
에 새로운 기록이 생성되면서 동작 중인것을 확인 할 수 있습니다.
첫번째 방법은 앞서 확인한 Pipeline Steps
를 확인하는 것입니다. 다시한번 확인하는 방법을 설명합니다.
Build History
에서 최신 빌드를 클릭합니다.Pipeline Steps
를 클릭하면 Pipeline 수행 스텝을 확인할 수 있습니다.현재 수행중인 Pipeline이 어떤 단계가 수행중인지 각 스탭별로 확인할 수 있고 상태를 확인할 수 있습니다.
두번째 방법은 출력되는 콘솔 로그를 확인하는 것입니다. Jenkins에서 빌드를 수행하면 빌드 수행 스크립트가 내부에 임시적으로 생성되어 작업을 실행합니다. 이때 발생되는 로그는 Console Output
을 통해 거의 실시간으로 동작을 확인 할 수 있습니다.
Build History
에서 최신 빌드에 마우스 포인터를 가져가면 우측에 드롭박스가 생깁니다. 또는 해당 히스토리를 클릭합니다.Console Output
나 클릭된 빌드 히스토리 상태에서 Console Output
를 클릭하면 수행중인 콘솔상의 출력을 확인합니다.마지막으로는 Pipeline을 위한 UI인 BlueOcean
플러그인을 활용하는 방법입니다. Blue Ocean은 Pipeline에 알맞은 UI를 제공하며 수행 단계와 각 단게별 결과를 쉽게 확인할 수 있습니다.
Jenkins 관리
에서 플러그인 관리
를 선택합니다.설치 가능
탭에서 Blue Ocean
을 선택하여 재시작 없이 설치
를 클릭 합니다.Blue Ocean
플러그인만 선택하여 설치하더라도 관련 플러그인들이 함께 설치 진행됩니다.Blue Ocean
항목을 확인 할 수 있습니다.Git SCM을 기반으로 Pipeline을 설정하는 과정을 설명합니다. 이를 이를 위한 Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 03-02.PollingSCMforBuildTriggering)
해당 과정을 수행하기 위해서는 다음의 구성이 필요합니다.
Jenkins가 구성된 호스트에 git 설치
$ yum -y install git
+
Jenkins 설정
Jenkins 관리
의 Global Tool Configuration
클릭Path to Git executable
칸에 Path 입력 (e.g. /usr/bin/git)Pipeline을 다음과 같이 설정합니다.
추가로 빌드 트리거를 위한 설정을 합니다.
Build Triggers
의 Poll SCM
활성화
Schedule 등록
# min hour day month day_of_week
+* * * * *
+# will run every minute on the minute
+
Polling으로 인한 빌드 트리거가 동작하면 좌측 메뉴의 Polling Log
에서 상태 확인이 가능합니다.
1분마다 확인 하도록 되어있기 때문에 다시 Polling을 시도하지만 변경사항이 없는 경우에는 Polling Log에 No changes
메시지가 나타나고 빌드는 수행되지 않습니다.
GitHub를 통한 CI 과정을 설명합니다. WebHook의 설정과 Jenkins에 관련 설정은 어떻게 하는지 알아봅니다.
Jenkins에서 접속가능하도록 GitHub에서 Token을 생성합니다.
github.com에 접속하여 로그인합니다.
우측 상단의 드롭박스에서 Settings
선택 후 좌측 메뉴 맨 아래의 Developer settings
를 선택합니다.
Developer settings
화면에서 좌측 메뉴 하단 Personal access tockes
를 클릭하고, 화면이 해당 페이지로 변경되면 Generate new token
버튼을 클릭합니다.
Token description에 Token설명을 입력하고 입니다. (e.g. jenkins-integration) 생성합니다. 생성시 repo
, admin:repo_hook
, notifications
항목은 활성화 합니다.
Generate token
버튼을 클릭하여 Token 생성이 완료되면 발급된 Token을 확인 할 수 있습니다. 해당 값은 Jenkins에서 Git연동설정 시 필요합니다.
우선 Jenkins에 Git연동을 위한 설정을 추가합니다.
Jenkins 관리
에서 시스템 설정
을 클릭합니다.GitHub
항목의 GitHub Servers
의 Add GitHub Server > GitHub Server
를 선택합니다.ADD
트롭박스를 선택합니다. Secret text
로 선택합니다. ADD
버튼 클릭하여 새로운 Credendial을 추가합니다.시스템 설정
화면으로 나오면 Credentials의 -none-
드롭박스에 추가한 Credential을 선택합니다.TEST CONNECTION
버튼을 클릭하여 정상적으로 연결이 되는지 확인합니다. Credentials verified for user Great-Stone, rate limit: 4998
와같은 메시지가 출력됩니다.git repo의 Webhook 을 통한 빌드를 수행합니다. GitHub에 다음과 같이 설정합니다.
https://github.com/Great-Stone/jenkins-git 를 fork
합니다.
우측 상단의 드롭박스에서 Settings
선택 후 좌측 메뉴 맨 아래의 Developer settings
를 선택합니다.
Developer settings
화면에서 좌측 메뉴 하단 Personal access tockes
를 클릭하고, 화면이 해당 페이지로 변경되면 Generate new token
버튼을 클릭합니다.
Token description에 Token설명을 입력하고 입니다. (e.g. jenkins-webhook) 생성합니다. 생성시 repo
, admin:repo_hook
, notifications
항목은 활성화 합니다.
Generate token
버튼을 클릭하여 Token 생성이 완료되면 발급된 Token을 확인 할 수 있습니다. 해당 값은 Jenkins에서 Git연동설정 시 필요합니다.
Webhook을 위한 Pipeline
타입의 Item을 추가로 생성합니다. (e.g. 03-04.WebhookBuild Triggering)
설정은 다음과 같이 수행합니다.
Pipeline
설정의 Definition
의 드롭다운을 선택하여 Pipeline script from SCM
을 선택합니다.
SCM
항목은 Git
을 선택하고 하위 필드를 다음과 같이 정의합니다.
Repositories :
Repository URL
을 입력하는데, GitHub에서 git url을 얻기위해서는 웹브라우저에서 해당 repository로 이동하여 Clone or download
버튼을 클릭하여 Url을 복사하여 붙여넣습니다.
Credentials : ADD
트롭박스를 선택합니다.
Username with password
로 선택합니다. ADD
버튼 클릭하여 새로운 Credendial을 추가합니다.시스템 설정
화면으로 나오면 Credentials의 -none-
드롭박스에 추가한 Credential을 선택합니다.Script Path : Pipeline 스크립트가 작성된 파일 패스를 지정합니다. 예제 소스에서는 root 위치에 Jenkinsfile
로 생성되어있으므로 해당 칸에는 Jenkinsfile
이라고 입력 합니다.
저장 후 좌측 메뉴의 Build Now
를 클릭하면 SCM에서 소스를 받고 Pipeline을 지정한 스크립트로 수행하는 것을 확인 할 수 있습니다.
빌드를 수행하기 위한 Worker로 다중 Jenkins를 컨트롤 할 수 있습니다. 이때 명령을 수행하는 Jenkins는 Master
, 빌드를 수행하는 Jenkins는 Worker
로 구분합니다. 여기서는 Worker의 연결을 원격 호스트의 Jenkins를 SSH를 통해 연결하는 방식과 컨테이너로 구성된 Jenkins를 연결하는 과정을 확인 합니다.
Master-Slave 방식, 또는 Master-Agent 방식으로 표현합니다.
팁
※ Slave 호스트에 Jenkins를 설치할 필요는 없습니다.
Worker가 실행되는 Slave 호스트에 SSH key를 생성하고 Worker 호스트에 인증 키를 복사하는 과정은 다음과 같습니다.
키 생성 및 복사(jenkins 를 수행할 유저를 생성해야 합니다.)
# User가 없는 경우 새로운 Jenkins slave 유저 추가
+$ useradd jenkins
+$ passwd jenkins
+Changing password for user jenkins.
+New password:
+Retype new password:
+
+# Slave 호스트에서 ssh 키를 생성합니다.
+$ ssh-keygen -t rsa
+Generating public/private rsa key pair.
+Enter file in which to save the key (/root/.ssh/id_rsa): <enter>
+Created directory '/root/.ssh'.
+Enter passphrase (empty for no passphrase): <enter>
+Enter same passphrase again: <enter>
+Your identification has been saved in /root/.ssh/id_rsa.
+Your public key has been saved in /root/.ssh/id_rsa.pub.
+The key fingerprint is: <enter>
+SHA256:WFU7MRVViaU1mSmCA5K+5yHfx7X+aV3U6/QtMSUoxug root@jenkinsecho.gyulee.com
+The key's randomart image is:
++---[RSA 2048]----+
+| .... o.+.=*O|
+| .. + . *o=.|
+| . .o. +o. .|
+| . o. + ... +|
+| o.S. . +.|
+| o oE .oo.|
+| = o . . +o=|
+| o . o ..o=|
+| . ..o+ |
++----[SHA256]-----+
+
+$ cd ~/.ssh
+$ cat ./id_rsa.pub > ./authorized_keys
+
Jenkins 관리
의 노드 관리
를 선택합니다.
좌측 메뉴에서 신규 노드
를 클릭합니다.
노드명에 고유한 이름을 입력하고 Permanent Agent
를 활성화 합니다.
새로운 노드에 대한 정보를 기입합니다.
Use this node as much as possible
Launch agent agents via SSH
로 설정합니다. ADD > Jenkins
를 클릭합니다.SSH Username with private key
를 선택합니다.~/.ssh/id_rsa
의 내용을 붙여넣어줍니다. (일반적으로 -----BEGIN RSA PRIVATE KEY-----
로 시작하는 내용입니다.)Non verifying verification strategy
를 선택합니다.빌드 실행 상태
에 새로운 Slave Node가 추가됨을 확인 할 수 있습니다.Label 지정한 Slave Worker에서 빌드가 수행되도록 기존 02-02.Jobs의 Pipeline 스크립트를 수정합니다. 기존 agent any
를 다음과 같이 agent { label 'Metal' }
로 변경합니다. 해당 pipeline은 label이 Metal
로 지정된 Worker에서만 빌드를 수행합니다.
pipeline {
+ agent { label 'Metal' }
+ parameters {
+ string(name: 'Greeting', defaultValue: 'Hello', description: 'How should I greet the world?')
+ }
+ stages {
+ stage('Example') {
+ steps {
+ echo "${params.Greeting} World!"
+ }
+ }
+ }
+}
+
Master Jenkins 호스트에서 docker 서비스에 설정을 추가합니다. docker 설치가 되어있지 않은 경우 설치가 필요합니다.
$ yum -y install docker
+
RHEL8 환경이 Master인 경우 위와 같은 방식으로 설치를 진행하면 변경된 패키지에 따라
podman-docker
가 설치 됩니다. 아직 Jenkins에서는 2019년 7월 29일 기준podman
을 지원하지 않음으로 별도 yum repository를 추가하여 진행합니다.docker-ce
최신 버전에서는containerd.io
의 필요 버전이1.2.2-3
이상이나 RHEL8에서 지원하지 않음으로 별도로 버전을 지정하여 설치합니다.$ yum -y install yum-utils +$ yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo +$ sudo yum repolist -v +... +Repo-id : docker-ce-stable +Repo-name : Docker CE Stable - x86_64 +Repo-revision: 1564098258 +Repo-updated : Fri 26 Jul 2019 08:44:18 AM KST +Repo-pkgs : 47 +Repo-size : 982 M +Repo-baseurl : https://download.docker.com/linux/centos/7/x86_64/stable +Repo-expire : 172,800 second(s) (last: Thu 25 Jul 2019 07:33:33 AM KST) +Repo-filename: /etc/yum.repos.d/docker-ce.repo +... + +$ yum -y install docker-ce-3:18.09.1-3.el7 +$ systemctl enable docker +$ systemctl start docker +
docker를 설치 한 뒤 API를 위한 TCP 포트를 활성화하는 작업을 진행합니다./lib/systemd/system/docker.service
에 ExecStart
옵션 뒤에 다음과 같이 -H tcp://0.0.0.0:4243
을 추가합니다.
...
+[Service]
+Type=notify
+# the default is not to use systemd for cgroups because the delegate issues still
+# exists and systemd currently does not support the cgroup feature set required
+# for containers run by docker
+ExecStart=/usr/bin/dockerd -H fd:// -H tcp://0.0.0.0:4243
+ExecReload=/bin/kill -s HUP $MAINPID
+TimeoutSec=0
+RestartSec=2
+Restart=always
+...
+
수정 후 서비스를 재시작합니다.
$ systemctl daemon-reload
+$ systemctl restart docker
+$ docker ps
+CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
+
+$ usermod -aG docker jenkins
+$ chmod 777 /var/run/docker.sock
+
Jenkins에 새로운 플러그인을 추가하고 설정합니다.
Jenkins 관리
로 이동하여 플러그인 관리
를 클릭합니다.
설치 가능
탭을 클릭하고 상단의 검색에 docker
를 입력하면 docker
플러그인이 나타납니다. 선택하여 설치를 진행하고 Jenkins를 재시작 합니다.
Jenkins 관리
로 이동하여 시스템 설정
을 클릭합니다.
Cloud
항목 아래 ADD A NEW CLOUD
드롭박스에 있는 Docker
를 선택합니다.
Name은 기본 값으로 진행하고 DOCKER CLOUD DETAILS...
버튼을 클릭합니다.
Docker Host URI : 앞서 설정한 port로 연결합니다. (e.g. tcp://master:4243)
TEST CONNECTION
버튼을 눌러 정상적으로 Version 정보와 API Version이 표기되는지 확인합니다.
Version = 18.09.1, API Version = 1.39
+
Enabled를 활성화 합니다.
Docker 실행을 위한 Item을 생성합니다. (e.g. 04-02.UsingDockerImagesForAgents)
Pipeline
스크립트를 구성합니다.pipeline {
+ agent {
+ docker { image 'node:latest' }
+ }
+ stages {
+ stage('Test'){
+ steps {
+ sh 'node --version'
+ }
+ }
+ }
+}
+
수정 후 좌측 Build Now
를 클릭하여 빌드 수행 후 결과를 확인합니다.
Step 1
에서의 결과와는 달리 Stage View
항목과 Pipeline stage가 수행된 결과를 확인할 수 있는 UI가 생성됩니다.
Freestyle project
형태의 Item을 생성합니다. (e.g. 04-03.ConfiguringSpecificAgents)
Jenkins는 각 단계, 빌드, 그리고 빌드 후 작업일 지정할 수 있습니다.
Freestyle project
에서는 이같은 전체 빌드 단계를 구성하고 여러가지 플러그인을 사용할 수 있는 환경을 제공합니다.
General
Restrict where this project can be run : 빌드 수행을 특정 Label 노드에 제한하도록 설정할 수 있습니다.
Label Expression : 앞서의 과정에서 생성한 노드 Metal
을 지정해봅니다. 해당 조건의 노드가 존재하는 경우 노드 개수 정보가 표기됩니다.
Label Metal is serviced by 1 node.
+
Build
ADD BUILD STEP
드롭박스에서 Excute shell
항목을 선택하여 추가 합니다. echo "Hello world."
를 넣어봅니다.ADD BUILD STEP
드롭박스에서 Excute shell
항목을 선택하여 추가 합니다. ls -al"
를 넣어봅니다.저장하고 좌측의 Build Now
를 클릭하여 빌드를 수행합니다.
콘솔 출력을 확인하면 지정한 Label 노드에서 각 빌드 절차가 수행된 것을 확인할 수 있습니다.
Jenkins가 유용한 툴인 이유중 하나는 방대한 양의 플러그인 입니다. Jenkins의 기능을 확장시키고, 관리, 빌드 정책 등을 확장 시켜주고, 타 서비스와의 연계를 쉽게 가능하도록 합니다.
Jenkins는 온라인에 연결된 plugin을 검색, 설치할 수 있는 플러그인 관리
기능을 갖고 있습니다. 좌측 메뉴에서 Jenkins 관리
를 클릭하면 플러그인 관리
링크를 통하여 해당 기능에 접근할 수 있습니다.
.hpi
확장자를 갖는 플러그인을 설치하거나 업데이트 사이트를 지정할 수 있습니다.각 플러그인 이름을 클릭하면 플러그인 정보를 확인할 수 있는 plugins.jenkins.io
사이트로 이동하여 정보를 보여줍니다. 사용방법은 우측에 wiki
링크를 클릭합니다. 대략적인 UI나 사용방법은 wiki.jenkins.io
에서 제공합니다.
Jenkins Pipeline의 Shared libraries에 대한 상세 내용은 다음 링크를 참고합니다. https://jenkins.io/doc/book/pipeline/shared-libraries/
이번 실습을 진행하기전에 GitHub에서 https://github.com/Great-Stone/evenOdd repository를 본인 계정의 GitHub에 Fork 하여 진행합니다.
소스의 var
디렉토리에는 Pipeline에서 사용하는 Shared Library들이 들어있습니다. groovy 스크립트로 되어있으며 Pipeline을 구성한 jenkinsfile
에서 이를 사용합니다.
vars/evenOdd.groovy
를 호출하고 값을 받아오는 형태를 갖고, evenOdd.groovy에서 사용하는 log.info
와 log.warning
은 vars/log.groovy
에 구현되어있습니다.
다음과 같이 Jenkins에 설정을 수행합니다.
Jenkins 관리
클릭 후 시스템 설정
을 선택합니다.Global Pipeline Libraries
의 추가 버튼을 클릭하여 새로운 구성을 추가합니다. Source Code Management
항목이 추가됩니다.GitHub
를 클릭하여 내용을 채웁니다. https://github.com/Great-Stone/evenOdd
인 경우 Great-Stone
이 Owner가 됩니다.Library
에 있는 Load implicitly
를 활성화 합니다.Shared Libraries가 준비가 되면 Pipeline
타입의 Item을 생성하고 (e.g. 05-02.UsingSharedLibraries) Pipeline 설정을 추가합니다.
저장 후 Build Now
를 클릭하여 빌드를 수행합니다. 빌드의 결과로는 2 단계로 수행되는데 1단계는 Declarative: Checkout SCM
으로 SCM으로부터 소스를 받아 준비하는 단계이고, 2단계는 jenkinsfile
을 수행하는 단계입니다. vars/evenOdd.goovy
스크립트에는 stage가 두개 있으나 해당 Pipeline 을 호출하는 값에 따라 하나의 stage만을 수행하도록 되어있어서 하나의 stage가 수행되었습니다.
// Jenkinsfile
+//@Library('evenOdd') _
+
+evenOdd(currentBuild.getNumber())
+
currentBuild.getNumber()
는 현재 생성된 Pipeline Item의 빌드 숫자에 따라 값을 evenOdd(빌드 숫자)
형태로 호출하게 됩니다.
Jenkins shared libraries를 사용하는 가장 좋은 예는 재사용성 있는 Groovy 함수를 타 작업자와 공유하는 것 입니다. 빌드의 상태는 다른 파이프 라인 단계로 계속할 것인지 결정하는 데 사용할 수도 있습니다.
경고
해당 설정은 모든 빌드에 영향을 주기 때문에 타 작업을 위해 추가된 Global Pipeline Libraries의 Library를 삭제하여 진행합니다.
Jenkins빌드의 결과를 받아볼 수 있는 몇가지 방안에 대해 알아봅니다.
Jenkins에서는 플러그인이나 외부 툴에 의해 빌드에 대한 결과를 받아 볼 수 있습니다. 대표적으로는 Jenkins의 슬랙 플러그인을 사용하여 슬랙으로 빌드에 결과를 받아보거나, catlight.io 에서 데스크탑용 어플리케이션에 연동하는 방법도 있습니다.
여기서는 Chrome 확장 프로그램을 통한 알림을 받을 수 있는 설정을 설명합니다. 이 과정을 진행하기 위해서는 Chrome 웹브라우저가 필요합니다.
chrome://apps/에 접속하여 앱스토어를 클릭합니다.
jenkins를 검색하여 Yet Another Jenkins Notifier
를 확인합니다. Chrome에 추가
버튼으로 확장 프로그램을 설치합니다.
설치가 완료되면 브라우저 우측 상단에 Jenkins 아이콘이 나타납니다. 클릭합니다.
각 Item(Job)의 url을 입력하여 +
버튼을 클릭합니다.
등록된 Item을 확인하고 해당 빌드를 Jenkins 콘솔에서 실행해봅니다. 결과에 대한 알림이 발생하는 것을 확인 할 수 있습니다.
Jenkins에서 빌드가 수행된 결과를 SCM에 반영하는 기능도 플러그인을 통해 가능합니다. SCM에서 해당 Jenkins에 접근이 가능해야 하므로 Jenkins는 SCM에서 접근가능한 네트워크 상태여야 합니다.
Jenkins에 새로운 플러그인을 추가하고 설정합니다.
Jenkins 관리
로 이동하여 플러그인 관리
를 클릭합니다.설치 가능
탭을 클릭하고 상단의 검색에 embed
를 입력하면 Embeddable Build Status
플러그인이 나타납니다. 선택하여 설치를 진행합니다.Jenkins 관리
로 이동하여 Configure Global Security
을 클릭합니다.Authorization
항목에서 Matrix-based security
를 체크합니다.Authenticated Users
의 경우 필요한 각 항목에 대해 체크박스를 활성화 합니다.Anonymous Users
에 대해서 Job/ViewStatus
항목을 활성화 합니다.이제 기존의 외부 SCM이 연결된 Item을 선택합니다. 여기서는 05-02.UsingSharedLibraries
에 설정합니다. 해당 Item을 선택하면 좌측에 Embeddable Build Status
항목이 새로 생긴것을 확인 할 수 있습니다.
해당 항목을 클릭하고 Markdown
의 unprotected
의 항목을 복사합니다.
[![Build Status](http://myjenkins.com/buildStatus/icon?job=05-02.UsingSharedLibraries)](http://myjenkins.com/job/05-02.UsingSharedLibraries/)
+
복사한 형식을 GitHub의 evenOdd repository의 README.md 파일 상단에 위치 시킵니다.
# evenOdd
+[![Build Status](http://myjenkins.com/buildStatus/icon?job=libraries)](http://myjenkins.com/job/libraries/)
+
+A Jenkins even/odd playbook from the Jenkins.io documentation
+
+Add this as a shared library called evenOdd in your jenkins
+instance, and then instantiate the pipeline in your project Jenkinsfile
+
+This will also use an example of global variabls from the log.groovy
+definitions
+
+
이같이 반영하면 각 빌드에 대한 결과를 SCM에 동적으로 상태를 반영 할 수 있습니다.
이같은 알림 설정은 코드의 빌드가 얼마나 잘 수행되는지 이해하고 추적할 수 있도록 도와줍니다.
테스트 Pipeline 구성시 테스트 과정을 지정할 수 있습니다. Testing을 위한 Pipeline
타입의 Item을 추가로 생성합니다. (e.g. 07-01.CodeCoverageTestsAndReports)
설정은 다음과 같이 수행합니다.
Pipeline
스크립트에 다음과 같이 입력 합니다. 테스트와 빌드, 검증 후 결과를 보관하는 단계까지 이루어 집니다.
pipeline {
+ agent any
+ stages {
+ stage('Build') {
+ steps {
+ sh '''
+ echo This > app.sh
+ echo That >> app.sh
+ '''
+ }
+ }
+ stage('Test') {
+ steps {
+ sh '''
+ grep This app.sh >> ${BUILD_ID}.cov
+ grep That app.sh >> ${BUILD_ID}.cov
+ '''
+ }
+ }
+ stage('Coverage'){
+ steps {
+ sh '''
+ app_lines=`cat app.sh | wc -l`
+ cov_lines=`cat ${BUILD_ID}.cov | wc -l`
+ echo The app has `expr $app_lines - $cov_lines` lines uncovered > ${BUILD_ID}.rpt
+ cat ${BUILD_ID}.rpt
+ '''
+ archiveArtifacts "${env.BUILD_ID}.rpt"
+ }
+ }
+ }
+}
+
빌드가 완료되면 해당 Job화면을 리로드 합니다. Pipeline에 archiveArtifacts
가 추가되었으므로 해당 Job에서 이를 관리합니다.
해당 아카이브에는 코드 검증 후의 결과가 저장 됩니다.
테스트 결과에 따라 빌드를 중지시키는 Pipeline 스크립트를 확인합니다. Testing을 위한 Pipeline
타입의 Item을 추가로 생성합니다. (e.g. 07-02.UsingTestResultsToStopTheBuild)
설정은 다음과 같이 수행합니다.
Pipeline
스크립트에 다음과 같이 입력 합니다. 테스트와 빌드, 검증 후 결과를 보관하는 단계까지 이루어 집니다.
pipeline {
+ agent any
+ stages {
+ stage('Build') {
+ steps {
+ sh '''
+ echo This > app.sh
+ echo That >> app.sh
+ echo The Other >> app.sh
+ '''
+ }
+ }
+ stage('Test') {
+ steps {
+ sh '''
+ for n in This That Those
+ do if grep $n app.sh >> ${BUILD_ID}.cov
+ then exit 1
+ fi
+ done
+ '''
+ }
+ }
+ stage('Coverage'){
+ steps {
+ sh '''
+ app_lines=`cat app.sh | wc -l`
+ cov_lines=`cat ${BUILD_ID}.cov | wc -l`
+ echo The app has `expr $app_lines - $cov_lines` lines uncovered > ${BUILD_ID}.rpt
+ cat ${BUILD_ID}.rpt
+ '''
+ archiveArtifacts "${env.BUILD_ID}.rpt"
+ }
+ }
+ }
+}
+
저장을 하고 빌드를 수행하면, Pipeline 스크립트 상 Test
Stage에서 조건 만족 시 exit 1
를 수행하므로 빌드는 중간에 멈추게 됩니다.
Jenkins는 외부 서비스와의 연동이나 정보 조회를 위한 API를 제공합니다.
Jenkins REST API 테스트를 위해서는 Jenkins에 인증 가능한 Token을 취득하고 curl이나 Postman 같은 도구를 사용하여 확인 가능 합니다. 우선 Token을 얻는 방법은 다음과 같습니다.
Jenkins에 로그인 합니다.
우측 상단의 로그인 아이디에 마우스를 호버하면 드롭박스 버튼이 나타납니다. 설정
을 클릭합니다.
API Token
에서 Current token
을 확인합니다. 등록된 Token이 없는 경우 다음과 같이 신규 Token을 발급 받습니다.
ADD NEW TOKEN
을 클릭합니다.
이름을 기입하는 칸에 로그인 한 아이디를 등록합니다. (e.g. admin)
GENERATE
를 클릭하여 Token을 생성합니다.
이름과 Token을 사용하여 다음과 같이 curl로 접속하면 Jenkins-Crumb
프롬프트가 나타납니다.
$ curl --user "admin:TOKEN" 'http://myjenkins.com/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,":",//crumb)'
+
+Jenkins-Crumb:89e1fd9c402824c89465f6b97f49b605
+
Crumb
를 확인했으면 다시 헤더 값에 Jenkins-Crumb:
를 추가하여 02-04.MultiStep
Job을 빌드하기 위해 다음과 같이 요청합니다.
$ curl -X POST http://myjenkins.com/job/02-04.MultiStep/build --user gyulee:11479bdec9cada082d189938a3946348be --data-urlencode json='' -H "Jenkins-Crumb:89e1fd9c402824c89465f6b97f49b605"
+
API로 호출된 빌드가 수행되어 빌드 번호가 증가하는 것을 확인합니다.
빌드에 대한 결과를 REST API를 통해 요청하는 방법을 알아봅니다. 앞서 진행시의 Token값이 필요합니다. Json 형태로 출력되기 때문에 정렬을 위해 python이 설치 되어있다면 mjson.tool
을 사용하여 보기 좋은 형태로 출력 가능합니다.
# Python이 설치되어있지 않은 경우
+$ yum -y install python2
+
+# Jenkins에 REST API로 마지막 빌드 상태 요청
+$ curl -s --user gyulee:11479bdec9cada082d189938a3946348be http://myjenkins.com/job/02-04.MultiStep/lastBuild/api/json | python2 -mjson.tool
+
+{
+ "_class": "org.jenkinsci.plugins.workflow.job.WorkflowRun",
+ "actions": [
+ {
+ "_class": "hudson.model.CauseAction",
+ "causes": [
+ {
+ "_class": "hudson.model.Cause$UserIdCause",
+ "shortDescription": "Started by user GyuSeok.Lee",
+ "userId": "gyulee",
+ "userName": "GyuSeok.Lee"
+ }
+ ]
+ },
+ {},
+ {
+ "_class": "hudson.plugins.git.util.BuildData",
+ "buildsByBranchName": {
+ "master": {
+ "_class": "hudson.plugins.git.util.Build",
+ "buildNumber": 5,
+ "buildResult": null,
+...
+
사용자별 배포수행을 위한 사용자 설정을 설명합니다.
Jenkins 관리
로 이동하여 Configure Global Security
를 클릭합니다.Enable security
는 보안 설정 여부를 설정하는 항목으로 기본적으로는 비활성화되어있습니다. 체크하여 활성화하면 다양한 보안 옵션을 설정할 수 있는 항목이 표기 됩니다.
Security Realm 에서는 Jenkins에서 사용하는 사용자 관리 방식을 선택합니다.
사용자의 가입 허용
이 활성화되면 Jenkins 에 접속하는 사용자는 스스로 계정을 생성하고 접근 가능합니다.Authorization 에서는 사용자 권한에 대한 설정을 정의합니다.
1.164
이전 버전의 동작과 동일하게 관리됩니다. Admin
사용자만 모든 기능을 수행하며, 일반 사용자와 비로그인 사용자는 읽기만 가능합니다.다음은 권한 매트릭스의 항목과 권한별 설명입니다.
항목 | 권한 | 의미 |
---|---|---|
Overall | Administer | 시스템의 전역 설정을 변경할 수 있다. OS 에서 허용된 범위안에서 전체 시스템 엑세스드의 매우 민감한 설정을 수행 |
Read | 젠킨스의 모든 페이지 확인 가능 | |
RunScripts | 그루비 콘솔이나 그루비 CLI 명령을 통해 그루비 스크립트를 실행 | |
UploadPlugins | 특정 플러그인을 업로드 | |
ConfigureUpdateCenter | 업데이트 사이트와 프록시 설정 | |
Slave | Configure | 기존 슬레이브 설정 가능 |
Delete | 기존 슬레이브 삭제 | |
Create | 신규 슬레이브 생성 | |
Disconnect | 슬레이브 연결을 끊거나 슬레이브를 임시로 오프라인으로 표시 | |
Connect | 슬레이브와 연결하거나 슬레이브를 온라인으로 표시 | |
Job | Create | 새로운 작업 생성 |
Delete | 기존 작업 삭제 | |
Configure | 기존 작업의 설정 갱신 | |
Read | 프로젝트 설정에 읽기 전용 권한 부여 | |
Discover | 익명 사용자가 작업을 볼 권한이 없으면 에러 메시지 표시를 하지 않고 로그인 폼으로 전환 | |
Build | 새로운 빌드 시작 | |
Workspace | 젠킨스 빌드를 실행 하기 위해 체크아웃 한 작업 영역의 내용을 가져오기 가능 | |
Cancel | 실행중인 빌드 취소 | |
Run | Delete | 빌드 내역에서 특정 빌드 삭제 |
Update | 빌드의 설명과 기타 프로퍼티 수정(빌드 실패 사유등) | |
View | Create | 새로운 뷰 생성 |
Delete | 기존 뷰 삭제 | |
Configure | 기존 뷰 설정 갱신 | |
Read | 기존 뷰 보기 | |
SCM | Tag | 특정 빌드와 관련된 소스 관리 시스템에 태깅을 생성 |
CSRF Protection 항목에 있는 Prevent Cross Site Request Forgery exploits
항목은 페이지마다 nonce 또는 crumb 이라 불리우는 임시 값을 삽입하여 사이트 간 요청 위조 공격을 막을 수 있게 해줍니다. 사용방법은 위에서 REST API 에 대한 설명 시 crumb 값을 얻고, 사용하는 방법을 참고합니다.
Jenkins에서 Pipeline을 설정하는 경우 일부 보안적인 값이 필요한 경우가 있습니다. 예를 들면 Username
과 Password
같은 값입니다. 앞서의 과정에서 Credentials
를 생성하는 작업을 일부 수행해 보았습니다. 여기서는 생성된 인증 값을 Pipeline에 적용하는 방법을 설명합니다.
Pipeline
타입의 Item을 추가로 생성합니다. (e.g. 09-02.SecuringSecretCredentialsAndFiles) 설정은 다음과 같이 수행합니다.
Pipeline
스크립트에 다음과 같이 입력 합니다.
pipeline {
+ agent any
+ environment {
+ SECRET=credentials('jenkins-secret-text')
+ }
+ stages {
+ stage('Build') {
+ steps {
+ echo "${env.SECRET}"
+ }
+ }
+ }
+}
+
저장 후 Build Now
를 클릭하여 빌드를 수행하면 실패하게 되고 Console Output
에서 진행사항을 보면, Pipeline 스크립트에서 선언한 jenkins-secret-text
때문에 에러가 발생한 것을 확인할 수 있습니다.
좌측 상단의 Jenkins
버튼을 클릭하여 최상위 메뉴로 이동합니다.
좌측 메뉴의 Credentials
를 클릭하고 (global)
도메인을 클릭합니다.
좌측에 Add Credentials
를 클릭하여 새로운 항목을 추가합니다.
저장 후 다시 빌드를 수행하면 정상적으로 수행됩니다. 해당 값은 숨기기 위한 값이므로 Pipeline 스크립트에서 echo
로 호출하더라도 ****
이란 값으로 표기 됩니다.
이같은 방법은 Password같은 보안에 민감한 정보를 사용하기에 유용합니다.
Jenkins의 변화와 활동에 대한 감시를 위한 설정 방법을 설명합니다. Jenkins에 새로운 플러그인을 추가하고 설정합니다.
Jenkins 관리
로 이동하여 플러그인 관리
를 클릭합니다.설치 가능
탭을 클릭하고 상단의 검색에 audit
를 입력하면 Audit Trail
플러그인이 나타납니다. 선택하여 설치합니다.Jenkins 관리
로 이동하여 시스템 설정
을 클릭합니다.ADD LOGGER
드롭박스에서 Log File
을 선택하여 설정합니다. 저장 후 빌드나 Job의 설정 변경등의 작업을 수행하면, audit.log.0
으로 지정된 파일 경로에 생성됨을 확인 할 수 있습니다.
$ tail -f ./audit.log.0
+Jul 31, 2019 10:47:32,727 AM job/02-02.Jobs/ #12 Started by user GyuSeok.Lee
+Jul 31, 2019 10:47:42,738 AM /job/03-04.WebhookBuild Triggering/configSubmit by gyulee
+Jul 31, 2019 10:48:09,001 AM /configSubmit by gyulee
+
다양한 프로젝트를 관리하는 경우 관리상, 빌드 프로젝트를 관리해야할 필요성이 발생합니다. Jenkins에서 Forder 아이템을 생성하여 관리 편의성과 보안요소를 추가할 수 있습니다.
우선 테스트를 위한 사용자를 추가합니다.
Jenkins 관리
를 클릭하여 Manage Users
로 이동합니다.사용자 생성
을 클릭하여 새로운 사용자를 추가합니다. 다음으로 Forder 타임의 Item을 추가합니다.
새로운 Item
을 클릭하여 이름을 02-Project
로 예를 들어 지정하고, Forder를 클릭하여 OK
버튼을 클릭합니다.SAVE
버튼을 클릭하고 좌측 상단의 Jenkins
버튼을 클릭하여 최상위 페이지로 이동합니다.02-02.Jobs
에 마우스를 대면 드롭박스 메뉴를 확장할 수 있습니다. Move
를 클릭합니다.Jenkins >> 02-Project
를 선택하고 MOVE
버튼을 클릭합니다. 다시 최상위 메뉴로 오면 02-02.Jobs
가 사라진 것을 확인할 수 있습니다. 02
로 시작하는 다은 프로젝트도 같은 작업을 수행하여 이동시킵니다.02-Project
를 클릭하면 이동된 프로젝트들이 나타납니다.권한 설정을 하여 현재 Admin 권한의 사용자는 접근 가능하고 새로 생성한 tester는 접근불가하도록 설정합니다.
Folder에 접근하는 권한을 설정하기위해 Jenkins 관리
의 Configure Global Security
로 이동합니다.
Authorization항목의 Project-based Matrix Authorization Strategy
를 선택합니다.
ADD USER OR GROUP...
을 클릭하여 Admin 권한의 사용자를 추가합니다.
Admin 권한의 사용자에게는 모든 권한을 주고 Authenticated Users
에는 Overall의 Read
권한만 부여합니다.
생성한 02-Project
로 이동하여 좌측 메뉴의 Configure
를 클릭합니다.
Properties에 추가된 Enable project-based security
를 확성화하면 항목별 권한 관리 메트릭스가 표시됩니다. Job의 Build, Read, ViewStatus, Workspace를 클릭하고 View의 Read를 클릭하여 권한을 부여합니다.
로그아웃 후에 앞서 추가한 test
사용자로 로그인 하면 기본적으로 다른 프로젝트나 Item들은 권한이 없기 때문에 보이지 않고, 앞서 설정한 02-Project
폴더만 리스트에 나타납니다.
Jenkins의 인증 기능을 사용하여 보안적 요소를 구성할 수 있습니다. Audit 로그를 활용하여 사용자별 활동을 기록할 수도 있고 Folder를 활용하면 간단히 사용자/그룹에 프로젝트를 구분하여 사용할 수 있도록 구성할 수 있습니다.
빌드 이후 빌드의 결과를 기록하고 저장하는 방법을 설명합니다.
Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 10-01.CreatingAndStoringArtifacts)
Pipeline에 다음과 같이 스크립트를 추가합니다.
pipeline {
+ agent any
+ stages{
+ stage('Build') {
+ steps{
+ sh 'echo "Generating artifacts for ${BUILD_NUMBER}" > output.txt'
+ }
+ }
+ stage('Archive') {
+ steps {
+ archiveArtifacts artifacts: 'output.txt', onlyIfSuccessful: true
+ }
+ }
+ }
+}
+
Archive
Stage에 archiveArtifacts
스크립트가 동작하는 예제입니다. 이같은 Pipeline 스크립트 작성을 도와주는 툴을 추가로 확인해 봅니다.
Pipeline Syntax
링크를 클릭합니다.Sample Step
에서 archiveArtifacts: Archive the artifacts
를 선택합니다. 고급...
을 클릭합니다.GENERATE PIPELINE SCRIPT
를 클릭합니다.결과물을 확인하면 Pipeline 스크립트에 작성한 형태와 같은 것을 확인 할 수 있습니다.
좌측 메뉴의 Build Now
를 클릭하여 빌드 수행 후에 화면에 Artifacts 항목이 추가된 것을 확인할 수 있습니다. UI 상에는 마지막 빌드 결과가 강조되어 나오고 각 빌드에 대한 결과물은 각각의 빌드단계의 다운로드 버튼으로 확인하고 다운로드 할 수 있습니다.
빌드 이후 보관되는 파일에 대해 어떤 프로젝트, 어떤 빌드 에서 발생한 결과물인지 확인할 수 있는 핑거프린팅 기능을 설명합니다.
Step 1
의 프로젝트를 그대로 사용하거나 Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 10-02.FingerprintingForArtifactTracking)
Step 1
Pipeline 스크립트의 archiveArtifacts
에 fingerprint: true
를 추가합니다.
pipeline {
+ agent any
+ stages{
+ stage('Build') {
+ steps{
+ sh 'echo "Generating text artifacts: Build:${BUILD_NUMBER}" > output.txt'
+ }
+ }
+ stage('Archive') {
+ steps {
+ archiveArtifacts artifacts: 'output.txt', fingerprint: true, onlyIfSuccessful: true
+ }
+ }
+ }
+}
+
파일의 지문을 확인합니다.
첫번째 빌드를 수행하고 빌드 결과 아카이브 파일 output.txt
파일을 다운로드 받습니다. (파일을 우클릭하고 다른 이름으로 링크 저장...
or Download Linked File
을 클릭하여 파일을 받습니다.)
좌측 상단의 Jenkins
를 클릭하여 최상위 메뉴로 돌아갑니다.
좌측 메뉴의 파일 핑거프린트 확인
을 클릭합니다.
파일 선택
버튼을 클릭하여 앞서 다운로드한 파일을 선택하고 확인하기
버튼을 클릭합니다.
어떤 프로젝트의 몇번째 빌드에서 발생한 파일인지 확인합니다.
두번째 빌드를 수행하고 파일 핑거프린트를 확인해 봅니다.
빌드 번호 정보가 변경된 것을 확인합니다.
Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 11-01.AutomatingDeploymentWithPipelines)
Pipeline에 다음과 같은 스크립트를 입력합니다.
pipeline {
+ agent any
+ stages {
+ stage('Build') {
+ steps {
+ sh 'echo "Hello World"'
+ }
+ }
+ stage('Test') {
+ steps {
+ sh 'echo "Test Hello World!"'
+ }
+ }
+ }
+}
+
두개의 Stage를 갖는 Pipeline 스크립트입니다. Pipeline은 빌드 수행시의 각 단계를 구분하여 빌드의 과정을 확인하고 실패에 따른 단계별 확인이 가능합니다.
좌측 Build Now
를 클릭하여 빌드를 수행하면 빌드에 대한 결과는 Stage 별로 성공 실패의 여부와 로그를 확인할 수 있도록 Stage View
가 UI로 제공됩니다. Stage 별로 Stage View는 기록되며, Stage에 변경이 있거나 이름이 변경되는 경우에는 해당 UI에 변경이 발생하여 기존 Pipeline 기록을 보지 못할 수 있습니다.
Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 11-02.CreatingPipelineGates)
Pipeline에 다음과 같은 스크립트를 입력합니다.
pipeline {
+ agent any
+ stages {
+ stage('Build') {
+ steps {
+ sh 'echo "Hello World"'
+ }
+ }
+ stage('BuildMore'){
+ steps {
+ input message: "Shall we build more?"
+ sh '''
+ echo "We are approved; continue!"
+ ls -lah
+ '''
+ }
+ }
+ }
+}
+
개의 Stage를 갖는 Pipeline 스크립트입니다. 두번째 Stage에 input
스크립트가 있습니다. 이 스크립트가 추가되면 Pipeline을 진행하면서 해당하는 동작을 수행할 것인지, 마치 승인 작업과 같은 동작을 수행할 수 있습니다.
좌측 Build Now
를 클릭하여 빌드를 수행하면 두번째 Stage에서 해당 작업을 수행할 지에 대한 물음을 확인 할 수 있습니다.
Abort
를 선택하면 빌드 취소와 같은 동작으로 실패로 처리되지는 않습니다.
빌드 단계를 구현할 때 Pipeline 스크립트로 하나의 프로젝트 내에서 모든 동작을 정의 할 수도 있지만 서로다른 Job을 연계하고, 승인 절차를 따르도록 구성할 수 있습니다.
Job promotion 기능을 사용하기 위한 플러그인을 설치합니다.
Jenkins 관리
에서 플러그인 관리
를 선택합니다.설치 가능
탭을 클릭하고 상단의 검색에 promoted
를 입력하면 promoted builds
를 확인 할 수 있습니다. 설치합니다.FreeStyle 타입의 Item을 생성합니다. (e.g. 11-03.Job-one)
General 탭의 Promote builds when...
를 활성화 하여 설정합니다.
Only when manually approved
활성화 ADD PRAMETER
드롭박스에서 Boolean Parameter
를 선택합니다. Build 드롭박스에서 Execute shell
을 선택합니다.
다음을 입력합니다.
echo 'This is the Job-one'
+
저장하면 생성된 프로젝트에 Promotion Status
항목이 추가되어 생성됩니다.
11-03.Job-one
빌드 후 승인에 대한 다음 빌드를 진행할 FreeStyle 타입의 Item을 생성합니다. (e.g. 11-03.Job-two)
빌드 유발 항목에서 Build when another project is promoted
를 활성화 합니다. 어떤 Job에서 promote 상황이 발생하였을 때 빌드를 수행할지 지정합니다.
Build 드롭박스에서 Execute shell
을 선택합니다.
다음을 입력합니다.
echo 'This is the Job-two'
+
11-03.Job-one
에 대한 빌드를 수행합니다. 수행 완료 후 빌드 히스토리의 최근 빌드를 클릭(e.g. #1)하면 Promotion Status
에 승인절차를 기다리고 있음을 확인할 수 있습니다. Parameters 항목의 approve
를 체크하고 APPROVE
버튼을 클릭합니다.
승인이 완료되면 해당 프로젝트의 승인에 대한 이벤트를 통해 빌드를 수행하는 11-03.Job-two
가 이어서 빌드됨을 확인 할 수 있습니다.
SCM의 Multibranch를 빌드하는 과정에 대해 설명합니다.
다음의 GitHub repository를 fork 합니다.
Multibranch Pipeline 형태의 Item을 생성합니다. (e.g. 11-04.MultibranchRepositoryAutomation)
ADD SOURCE
드롭박스에서 GitHub를 클릭합니다. VALIDATE
버튼을 클릭하여 잘 접근 되는지 확인합니다.Periodically if not otherwise run
를 활성화 합니다. 1 minute
으로 설정합니다.저장 후에는 자동적으로 모든 브랜치의 소스를 빌드 수행합니다.
SCM에서 브랜치를 여러개 관리하고 모두 빌드와 테스팅이 필요하다면 Multibranch 프로젝트를 생성하여 등록하고, 빌드 관리가 가능합니다.
Pipeline 을 스크립트를 작성하는 방법을 배워봅니다. Pipeline 타입의 Item을 생성합니다. (e.g. 11-05. CreatingPipelineWithSnippets)
Pipeline에 다음과 같은 스크립트를 입력합니다.
pipeline {
+ agent any
+ stages {
+ stage("Hello") {
+ steps {
+ echo 'Hello World'
+ }
+ }
+ }
+}
+
echo가 동작할때 시간을 기록하도록 스크립트를 수정해보겠습니다.
Pipeline Syntax 링크를 클릭합니다.
Sample Step에서 timestamps: timestamps
를 선택하고 GENERATE PIPELINE SCRIPT
버튼을 클릭합니다.
timestamps {
+ // some block
+}
+
사용방식을 확인하고 앞서 Pipeline 스크립트의 stage에 시간을 기록하도록 수정합니다.
...
+stage("Hello") {
+ steps {
+ timestamps {
+ echo 'Hello World'
+ }
+ }
+}
+...
+
빌드를 수행하고 로그를 확인해 봅니다. echo 동작이 수행 될때 시간이 함께 표기되는 것을 확인 할 수 있습니다.
Pipeline에서 사용할 수 있는 변수를 확인하고 사용하는 방법을 알아봅니다. Pipeline 타입의 Item을 생성합니다. (e.g. 11-06.DiscoveringGlobalPipelineVariables)
Pipeline에 다음과 같은 스크립트를 입력합니다.
pipeline {
+ agent any
+ stages {
+ stage('Build') {
+ steps {
+ echo "We are in build ${currentBuild.number}"
+ echo "Our current result is ${currentBuild.currentResult}"
+ }
+ }
+ stage('BuildMore'){
+ steps {
+ echo "Name of the project is ${currentBuild.projectName}"
+ }
+ }
+ stage('BuildEnv'){
+ steps {
+ echo "Jenkins Home : ${env.JENKINS_HOME}"
+ }
+ }
+ }
+}
+
Pipeline 스크립트에서 사용가능한 변수와 사용방법은 Pipeline Syntax
링크의 Global Variables Reference
항목에서 확인 가능합니다.
GitHub를 SCM으로 사용하는 경우 다음과 같은 메시지가 출력되면서 진행되지 않는 경우가 있습니다.
GitHub API Usage: Current quota has 5000 remaining (447380 over budget). Next quota of 5000 in 5 days 0 hr. Sleeping for 4 days 23 hr.
+14:07:33 GitHub API Usage: The quota may have been refreshed earlier than expected, rechecking...
+
이 경우 서버 시간과 GitHub의 시간이 맞지 않아 발생할 수 있는 이슈 입니다. ntpdate를 재설정 합니다.
RHEL7 : ntpd를 재시작 합니다.
$ systemctl restart ntpd
+
RHEL8 : RHEL8에서는 ntpdate를 사용하지 않고 chronyd가 대신합니다.
https://access.redhat.com/solutions/4130881
$ systemctl stop chronyd
+$ chronyd -q
+$ systemctl start chronyd
+
Update at 31 Jul, 2019
Jenkins Pipeline 을 구성하기 위해 VM 환경에서 Jenkins와 관련 Echo System을 구성합니다. 각 Product의 버전은 문서를 작성하는 시점에서의 최신 버전을 위주로 다운로드 및 설치되었습니다. 구성 기반 환경 및 버전은 필요에 따라 변경 가능합니다.
Category | Name | Version |
---|---|---|
VM | VirtualBox | 6.0.10 |
OS | Red Hat Enterprise Linux | 8.0.0 |
JDK | Red Hat OpenJDK | 1.8.222 |
Jenkins | Jenkins rpm | 2.176.2 |
Jenkins 실행 및 구성
Jenkins를 실행 및 구성하기위한 OS와 JDK가 준비되었다는 가정 하에 진행합니다. 필요 JDK 버전 정보는 다음과 같습니다.
필요 JDK를 설치합니다.
$ subscription-manager repos --enable=rhel-8-for-x86_64-baseos-rpms --enable=rhel-8-for-x86_64-appstream-rpms
+
+### Java JDK 8 ###
+$ yum -y install java-1.8.0-openjdk-devel
+
+### Check JDK version ###
+$ java -version
+openjdk version "1.8.0_222"
+OpenJDK Runtime Environment (build 1.8.0_222-b10)
+OpenJDK 64-Bit Server VM (build 25.222-b10, mixed mode)
+
Red Hatsu/Fedora/CentOS 환경에서의 Jenkins 다운로드 및 실행은 다음의 과정을 수행합니다.
repository를 등록합니다.
$ sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo
+$ sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key
+
작성일 기준 LTS 버전인 2.176.2
버전을 설치합니다.
$ yum -y install jenkins
+
패키지로 설치된 Jenkins의 설정파일은 /etc/sysconfig/jenkins
에 있습니다. 해당 파일에서 실행시 활성화되는 포트 같은 설정을 변경할 수 있습니다.
## Type: integer(0:65535)
+## Default: 8080
+## ServiceRestart: jenkins
+#
+# Port Jenkins is listening on.
+# Set to -1 to disable
+#
+JENKINS_PORT="8080"
+
외부 접속을 위해 Jenkins에서 사용할 포트를 방화벽에서 열어줍니다.
$ firewall-cmd --permanent --add-port=8080/tcp
+$ firewall-cmd --reload
+
서비스를 부팅시 실행하도록 활성화하고 Jenkins를 시작합니다.
$ systemctl enable jenkins
+$ systemctl start jenkins
+
실행 후 브라우저로 접속하면 Jenkins가 준비중입니다. 준비가 끝나면 Unlock Jenkins
페이지가 나오고 /var/lib/jenkins/secrets/initialAdminPassword
의 값을 입력하는 과정을 설명합니다. 해당 파일에 있는 토큰 복사하여 붙여넣습니다.
이후 과정은 Install suggested plugins
를 클릭하여 기본 플러그인을 설치하여 진행합니다. 경우에 따라 Select plugins to install
을 선택하여 플러그인을 지정하여 설치할 수 있습니다.
플러그인 설치 과정을 선택하여 진행하면 Getting Started
화면으로 전환되어 플러그인 설치가 진행됩니다.
설치 후 기본 Admin User
를 생성하고, 접속 Url을 확인 후 설치과정을 종료합니다.
GitHub 계정생성
진행되는 실습에서는 일부 GitHub를 SCM으로 연동합니다. 원활한 진행을 위해 GitHub계정을 생성해주세요. 또는 별개의 Git 서버를 구축하여 사용할 수도 있습니다.
Jenkins Theme (Optional)
Jenkins는 간단히 테마와 회사 CI를 적용할 수 있는 플러그인이 제공됩니다.
Jenkins 관리
로 이동하여 플러그인 관리
를 클릭합니다.
설치 가능
탭을 클릭하고 상단의 검색에 theme
를 입력하면 Login Theme
와 Simple Theme
를 확인 할 수 있습니다. 둘 모두 설치합니다.
로그아웃을 하면 로그인 페이지가 변경된 것을 확인 할 수 있습니다.
기본 Jenkins 테마를 변경하기 위해서는 다음의 과정을 수행합니다.
Build your own theme with a company logo!
에서 색상과 로고를 업로드 합니다.
DOWNLOAD YOUR THEME!
버튼을 클릭하면 CSS파일이 다운됩니다.
Jenkins 관리
로 이동하여 시스템 설정
를 클릭합니다.
Theme
항목의 Theme elements
의 드롭다운 항목에서 Extra CSS
를 클릭하고 앞서 다운받은 CSS파일의 내용을 붙여넣고 설정을 저장하면 적용된 테마를 확인할 수 있습니다.
CI/CD Concept Definitions
Delivery vs Deployment
Jenkins for CI/CD
Job and Project
프로젝트는 Job의 일부 입니다. 즉, 모든 프로젝트가 Job이지만 모든 Job이 프로젝트는 아닙니다. Job의 구조는 다음과 같습니다.
FreeStyleProejct, MatrixProject, ExternalJob만 New job
에 표시됩니다.
Step 1. New pipeline
Step 1에서는 stage
없이 기본 Pipeline을 실행하여 수행 테스트를 합니다.
Jenkins 로그인
좌측 새로운 Item
클릭
Enter an item name
에 Job 이름 설정 (e.g. 2.Jobs)
Pipeline
선택 후 OK
버튼 클릭
Pipeline
항목 오른 쪽 Try sample Pipelie...
클릭하여 Hello world
클릭 후 저장
node {
+ echo 'Hello World'
+}
+
좌측 Build now
클릭
좌측 Build History
의 최근 빌드된 항목(e.g. #1) 우측에 마우스를 가져가면 dropdown 버튼이 생깁니다. 해당 버튼을 클릭하여 Console Output
클릭
수행된 echo
동작 출력을 확인합니다.
Started by user GyuSeok.Lee
+Running in Durability level: MAX_SURVIVABILITY
+[Pipeline] Start of Pipeline
+[Pipeline] node
+Running on Jenkins in /var/lib/jenkins/workspace/2.Jobs
+[Pipeline] {
+[Pipeline] echo
+Hello World
+[Pipeline] }
+[Pipeline] // node
+[Pipeline] End of Pipeline
+Finished: SUCCESS
+
Step 2. New pipeline
Step 2에서는 stage
를 구성하여 실행합니다.
기존 생성한 Job 클릭 (e.g. 02-02.Jobs)
좌측 구성
을 클릭하여 Pipeline
스크립트를수정합니다.
pipeline{
+ agent any
+ stages {
+ stage("Hello") {
+ steps {
+ echo 'Hello World'
+ }
+ }
+ }
+}
+
수정 후 좌측 Build Now
를 클릭하여 빌드 수행 후 결과를 확인합니다.
Step 1
에서의 결과와는 달리 Stage View
항목과 Pipeline stage가 수행된 결과를 확인할 수 있는 UI가 생성됩니다.
수행된 빌드의 Console Output
을 확인하면 앞서 Step 1
에서는 없던 stage 항목이 추가되어 수행됨을 확인 할 수 있습니다.
Started by user GyuSeok.Lee
+Running in Durability level: MAX_SURVIVABILITY
+[Pipeline] Start of Pipeline
+[Pipeline] node
+Running on Jenkins in /var/lib/jenkins/workspace/2.Jobs
+[Pipeline] {
+[Pipeline] stage
+[Pipeline] { (Hello)
+[Pipeline] echo
+Hello World
+[Pipeline] }
+[Pipeline] // stage
+[Pipeline] }
+[Pipeline] // node
+[Pipeline] End of Pipeline
+Finished: SUCCESS
+
Step 3. Parameterizing a job
Pipeline 내에서 사용되는 매개변수 정의를 확인해 봅니다. Pipeline 스크립트는 다음과 같습니다.
pipeline {
+ agent any
+ parameters {
+ string(name: 'Greeting', defaultValue: 'Hello', description: 'How should I greet the world?')
+ }
+ stages {
+ stage('Example') {
+ steps {
+ echo "${params.Greeting} World!"
+ }
+ }
+ }
+}
+
parameters
항목내에 매개변수의 데이터 유형(e.g. string)을 정의합니다. name
은 값을 담고있는 변수이고 defaultValue
의 값을 반환합니다. Pipeline에 정의된 parameters
는 params
내에 정의 되므로 ${params.매개변수이름}
과 같은 형태로 호출 됩니다.
저장 후 다시 구성
을 확인하면 이 빌드는 매개변수가 있습니다
가 활성화 되고 내부에 추가된 매개변수 항목을 확인 할 수 있습니다.
이렇게 저장된 Pipeline Job은 매개변수를 외부로부터 받을 수 있습니다. 따라서 좌측의 기존 Build Now
는 build with Parameters
로 변경되었고, 이를 클릭하면 Greeting을 정의할 수 있는 UI가 나타납니다. 해당 매개변수를 재정의 하여 빌드를 수행할 수 있습니다.
Step 4. Creating multiple steps for a job
다중스텝을 위한 Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 02-04.MultiStep)
Pipeline에 다음과 같이 스크립트를 추가합니다.
pipeline {
+ agent any
+ stages {
+ stage('Build') {
+ steps {
+ sh 'echo "Hello World"'
+ sh '''
+ echo "Multiline shell steps works too"
+ ls -lah
+ '''
+ }
+ }
+ }
+}
+
'''
은 스크립트 정의 시 여러줄을 입력할 수 있도록 묶어주는 역할을 합니다. 해당 스크립트에서는 sh
로 구분된 스크립트 명령줄이 두번 수행됩니다.
실행되는 여러 스크립트의 수행을 stage
로 구분하기위해 기존 Pipeline 스크립트를 다음과 같이 수정합니다.
pipeline {
+ agent any
+ stages {
+ stage('Build-1') {
+ steps {
+ sh 'echo "Hello World"'
+ }
+ }
+ stage('Build-2') {
+ steps {
+ sh '''
+ echo "Multiline shell steps works too"
+ ls -lah
+ '''
+ }
+ }
+ }
+}
+
stage를 구분하였기 때문에 각 실행되는 sh
스크립트는 각 스테이지에서 한번씩 수행되며, 이는 빌드의 결과로 나타납니다.
Step 5. Adding scripts as a job step
Pipeline의 step을 추가하여 결과를 확인하는 과정을 설명합니다. 피보나치 수열을 수행하는 쉘 스크립트를 시간제한을 두어 수행하고 그 결과를 확인합니다.
Jenkins가 설치된 서버에 [피보나치 수열]([https://namu.wiki/w/피보나치 수열](https://namu.wiki/w/피보나치 수열))을 수행하는 스크립트를 작성합니다. Sleep이 있기 때문에 일정 시간 이상 소요 됩니다.
$ mkdir -p /var/jenkins_home/scripts
+$ cd /var/jenkins_home/scripts
+$ vi ./fibonacci.sh
+#!/bin/bash
+N=${1:-10}
+
+a=0
+b=1
+
+echo "The Fibonacci series is : "
+
+for (( i=0; i<N; i++ ))
+do
+ echo "$a"
+ sleep 2
+ fn=$((a + b))
+ a=$b
+ b=$fn
+done
+# End of for loop
+
+$ chown -R jenkins /var/jenkins_home/
+$ chmod +x /var/jenkins_home/scripts/fibonacci.sh
+
다중스텝을 위한 Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 02-05.AddingStep)
Pipeline에 다음과 같이 스크립트를 추가합니다.
pipeline {
+ agent any
+ stages {
+ stage('Deploy') {
+ steps {
+ timeout(time: 1, unit: 'MINUTES') {
+ sh '/var/jenkins_home/scripts/fibonacci.sh 5'
+ }
+ timeout(time: 1, unit: 'MINUTES') {
+ sh '/var/jenkins_home/scripts/fibonacci.sh 32'
+ }
+ }
+ }
+ }
+}
+
steps
에 스크립트를 timeout
이 감싸고 있으며, 각 스크립트의 제한시간은 1분입니다. 빌드를 수행하면 최종적으로는 aborted
, 즉 중단됨 상태가 되는데 그 이유는 빌드 기록에서 해당 빌드를 클릭하면 확인 가능합니다.
Build History
에서 최신 빌드를 클릭합니다.
좌측 Pipeline Steps
를 클릭하면 Pipeline 수행 스텝을 확인할 수 있습니다.
첫번째로 나타나는 /var/jenkins_home/scripts/fibonacci.sh 5
를 수행하는 Shell Script
의 콘솔창 버튼을 클릭하면 잘 수행되었음을 확인 할 수 있습니다.
두번째로 나타나는 /var/jenkins_home/scripts/fibonacci.sh 32
를 수행하는 Shell Script
의 콘솔창 버튼을 클릭하면 다음과 같이 중도에 프로세스를 중지한 것을 확인 할 수 있습니다.
+ /var/jenkins_home/scripts/fibonacci.sh 32
+The Fibonacci series is :
+0
+1
+1
+2
+3
+...
+317811
+514229
+Sending interrupt signal to process
+/var/jenkins_home/scripts/fibonacci.sh: line 16: 13543 Terminated sleep 2
+832040
+/var/lib/jenkins/workspace/02-05.AddingStep@tmp/durable-e44bb232/script.sh: line 1: 13109 Terminated /var/jenkins_home/scripts/fibonacci.sh 32
+script returned exit code 143
+
Step 1. Tracking build state
Pipeline이 수행되는 동작을 추적하는 과정을 확인합니다. 이를 이를 위한 Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 03-01.TrackingBuildState)
Pipeline에 다음과 같이 스크립트를 추가합니다.
pipeline {
+ agent any
+ stages {
+ stage('Deploy') {
+ steps {
+ timeout(time: 1, unit: 'MINUTES') {
+ sh 'for n in `seq 1 10`; do echo $n; sleep 1; done'
+ }
+ timeout(time: 1, unit: 'MINUTES') {
+ sh 'for n in `seq 1 50`; do echo $n; sleep 1; done'
+ }
+ }
+ }
+ }
+}
+
Build Now
를 클릭하여 빌드를 수행합니다. 그러면, 좌측의 Build History
에 새로운 기록이 생성되면서 동작 중인것을 확인 할 수 있습니다.
첫번째 방법은 앞서 확인한 Pipeline Steps
를 확인하는 것입니다. 다시한번 확인하는 방법을 설명합니다.
Build History
에서 최신 빌드를 클릭합니다.Pipeline Steps
를 클릭하면 Pipeline 수행 스텝을 확인할 수 있습니다.현재 수행중인 Pipeline이 어떤 단계가 수행중인지 각 스탭별로 확인할 수 있고 상태를 확인할 수 있습니다.
두번째 방법은 출력되는 콘솔 로그를 확인하는 것입니다. Jenkins에서 빌드를 수행하면 빌드 수행 스크립트가 내부에 임시적으로 생성되어 작업을 실행합니다. 이때 발생되는 로그는 Console Output
을 통해 거의 실시간으로 동작을 확인 할 수 있습니다.
Build History
에서 최신 빌드에 마우스 포인터를 가져가면 우측에 드롭박스가 생깁니다. 또는 해당 히스토리를 클릭합니다.Console Output
나 클릭된 빌드 히스토리 상태에서 Console Output
를 클릭하면 수행중인 콘솔상의 출력을 확인합니다.마지막으로는 Pipeline을 위한 UI인 BlueOcean
플러그인을 활용하는 방법입니다. Blue Ocean은 Pipeline에 알맞은 UI를 제공하며 수행 단계와 각 단게별 결과를 쉽게 확인할 수 있습니다.
Jenkins 관리
에서 플러그인 관리
를 선택합니다.설치 가능
탭에서 Blue Ocean
을 선택하여 재시작 없이 설치
를 클릭 합니다.Blue Ocean
플러그인만 선택하여 설치하더라도 관련 플러그인들이 함께 설치 진행됩니다.Blue Ocean
항목을 확인 할 수 있습니다.Step 2. Polling SCM for build triggering
Git SCM을 기반으로 Pipeline을 설정하는 과정을 설명합니다. 이를 이를 위한 Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 03-02.PollingSCMforBuildTriggering)
해당 과정을 수행하기 위해서는 다음의 구성이 필요합니다.
Jenkins가 구성된 호스트에 git 설치
$ yum -y install git
+
Jenkins 설정
Jenkins 관리
의 Global Tool Configuration
클릭Path to Git executable
칸에 Path 입력 (e.g. /usr/bin/git)Pipeline을 다음과 같이 설정합니다.
추가로 빌드 트리거를 위한 설정을 합니다.
Build Triggers
의 Poll SCM
활성화
Schedule 등록
# min hour day month day_of_week
+* * * * *
+# will run every minute on the minute
+
Polling으로 인한 빌드 트리거가 동작하면 좌측 메뉴의 Polling Log
에서 상태 확인이 가능합니다.
1분마다 확인 하도록 되어있기 때문에 다시 Polling을 시도하지만 변경사항이 없는 경우에는 Polling Log에 No changes
메시지가 나타나고 빌드는 수행되지 않습니다.
Step 3. Connecting Jenkins to GitHub
GitHub를 통한 CI 과정을 설명합니다. WebHook의 설정과 Jenkins에 관련 설정은 어떻게 하는지 알아봅니다.
Jenkins에서 접속가능하도록 GitHub에서 Token을 생성합니다.
github.com에 접속하여 로그인합니다.
우측 상단의 드롭박스에서 Settings
선택 후 좌측 메뉴 맨 아래의 Developer settings
를 선택합니다.
Developer settings
화면에서 좌측 메뉴 하단 Personal access tockes
를 클릭하고, 화면이 해당 페이지로 변경되면 Generate new token
버튼을 클릭합니다.
Token description에 Token설명을 입력하고 입니다. (e.g. jenkins-integration) 생성합니다. 생성시 repo
, admin:repo_hook
, notifications
항목은 활성화 합니다.
Generate token
버튼을 클릭하여 Token 생성이 완료되면 발급된 Token을 확인 할 수 있습니다. 해당 값은 Jenkins에서 Git연동설정 시 필요합니다.
우선 Jenkins에 Git연동을 위한 설정을 추가합니다.
Jenkins 관리
에서 시스템 설정
을 클릭합니다.GitHub
항목의 GitHub Servers
의 Add GitHub Server > GitHub Server
를 선택합니다.ADD
트롭박스를 선택합니다. Secret text
로 선택합니다. ADD
버튼 클릭하여 새로운 Credendial을 추가합니다.시스템 설정
화면으로 나오면 Credentials의 -none-
드롭박스에 추가한 Credential을 선택합니다.TEST CONNECTION
버튼을 클릭하여 정상적으로 연결이 되는지 확인합니다. Credentials verified for user Great-Stone, rate limit: 4998
와같은 메시지가 출력됩니다.Step 4. Webhook build triggering
git repo의 Webhook 을 통한 빌드를 수행합니다. GitHub에 다음과 같이 설정합니다.
https://github.com/Great-Stone/jenkins-git 를 fork
합니다.
우측 상단의 드롭박스에서 Settings
선택 후 좌측 메뉴 맨 아래의 Developer settings
를 선택합니다.
Developer settings
화면에서 좌측 메뉴 하단 Personal access tockes
를 클릭하고, 화면이 해당 페이지로 변경되면 Generate new token
버튼을 클릭합니다.
Token description에 Token설명을 입력하고 입니다. (e.g. jenkins-webhook) 생성합니다. 생성시 repo
, admin:repo_hook
, notifications
항목은 활성화 합니다.
Generate token
버튼을 클릭하여 Token 생성이 완료되면 발급된 Token을 확인 할 수 있습니다. 해당 값은 Jenkins에서 Git연동설정 시 필요합니다.
Webhook을 위한 Pipeline
타입의 Item을 추가로 생성합니다. (e.g. 03-04.WebhookBuild Triggering)
설정은 다음과 같이 수행합니다.
Pipeline
설정의 Definition
의 드롭다운을 선택하여 Pipeline script from SCM
을 선택합니다.
SCM
항목은 Git
을 선택하고 하위 필드를 다음과 같이 정의합니다.
Repositories :
Repository URL
을 입력하는데, GitHub에서 git url을 얻기위해서는 웹브라우저에서 해당 repository로 이동하여 Clone or download
버튼을 클릭하여 Url을 복사하여 붙여넣습니다.
Credentials : ADD
트롭박스를 선택합니다.
Username with password
로 선택합니다. ADD
버튼 클릭하여 새로운 Credendial을 추가합니다.시스템 설정
화면으로 나오면 Credentials의 -none-
드롭박스에 추가한 Credential을 선택합니다.Script Path : Pipeline 스크립트가 작성된 파일 패스를 지정합니다. 예제 소스에서는 root 위치에 Jenkinsfile
로 생성되어있으므로 해당 칸에는 Jenkinsfile
이라고 입력 합니다.
저장 후 좌측 메뉴의 Build Now
를 클릭하면 SCM에서 소스를 받고 Pipeline을 지정한 스크립트로 수행하는 것을 확인 할 수 있습니다.
빌드를 수행하기 위한 Worker로 다중 Jenkins를 컨트롤 할 수 있습니다. 이때 명령을 수행하는 Jenkins는 Master
, 빌드를 수행하는 Jenkins는 Worker
로 구분합니다. 여기서는 Worker의 연결을 원격 호스트의 Jenkins를 SSH를 통해 연결하는 방식과 컨테이너로 구성된 Jenkins를 연결하는 과정을 확인 합니다.
Master-Slave 방식, 또는 Master-Agent 방식으로 표현합니다.
※ Slave 호스트에 Jenkins를 설치할 필요는 없습니다.
Step 1. Adding an SSH build agent to Jenkins
Worker가 실행되는 Slave 호스트에 SSH key를 생성하고 Worker 호스트에 인증 키를 복사하는 과정은 다음과 같습니다.
키 생성 및 복사(jenkins 를 수행할 유저를 생성해야 합니다.)
# User가 없는 경우 새로운 Jenkins slave 유저 추가
+$ useradd jenkins
+$ passwd jenkins
+Changing password for user jenkins.
+New password:
+Retype new password:
+
+# Slave 호스트에서 ssh 키를 생성합니다.
+$ ssh-keygen -t rsa
+Generating public/private rsa key pair.
+Enter file in which to save the key (/root/.ssh/id_rsa): <enter>
+Created directory '/root/.ssh'.
+Enter passphrase (empty for no passphrase): <enter>
+Enter same passphrase again: <enter>
+Your identification has been saved in /root/.ssh/id_rsa.
+Your public key has been saved in /root/.ssh/id_rsa.pub.
+The key fingerprint is: <enter>
+SHA256:WFU7MRVViaU1mSmCA5K+5yHfx7X+aV3U6/QtMSUoxug root@jenkinsecho.gyulee.com
+The key's randomart image is:
++---[RSA 2048]----+
+| .... o.+.=*O|
+| .. + . *o=.|
+| . .o. +o. .|
+| . o. + ... +|
+| o.S. . +.|
+| o oE .oo.|
+| = o . . +o=|
+| o . o ..o=|
+| . ..o+ |
++----[SHA256]-----+
+
+$ cd ~/.ssh
+$ cat ./id_rsa.pub > ./authorized_keys
+
Jenkins 관리
의 노드 관리
를 선택합니다.
좌측 메뉴에서 신규 노드
를 클릭합니다.
노드명에 고유한 이름을 입력하고 Permanent Agent
를 활성화 합니다.
새로운 노드에 대한 정보를 기입합니다.
Use this node as much as possible
Launch agent agents via SSH
로 설정합니다. ADD > Jenkins
를 클릭합니다.SSH Username with private key
를 선택합니다.~/.ssh/id_rsa
의 내용을 붙여넣어줍니다. (일반적으로 -----BEGIN RSA PRIVATE KEY-----
로 시작하는 내용입니다.)Non verifying verification strategy
를 선택합니다.빌드 실행 상태
에 새로운 Slave Node가 추가됨을 확인 할 수 있습니다.Label 지정한 Slave Worker에서 빌드가 수행되도록 기존 02-02.Jobs의 Pipeline 스크립트를 수정합니다. 기존 agent any
를 다음과 같이 agent { label 'Metal' }
로 변경합니다. 해당 pipeline은 label이 Metal
로 지정된 Worker에서만 빌드를 수행합니다.
pipeline {
+ agent { label 'Metal' }
+ parameters {
+ string(name: 'Greeting', defaultValue: 'Hello', description: 'How should I greet the world?')
+ }
+ stages {
+ stage('Example') {
+ steps {
+ echo "${params.Greeting} World!"
+ }
+ }
+ }
+}
+
Step 2. Using Docker images for agents
Master Jenkins 호스트에서 docker 서비스에 설정을 추가합니다. docker 설치가 되어있지 않은 경우 설치가 필요합니다.
$ yum -y install docker
+
RHEL8 환경이 Master인 경우 위와 같은 방식으로 설치를 진행하면 변경된 패키지에 따라
podman-docker
가 설치 됩니다. 아직 Jenkins에서는 2019년 7월 29일 기준podman
을 지원하지 않음으로 별도 yum repository를 추가하여 진행합니다.docker-ce
최신 버전에서는containerd.io
의 필요 버전이1.2.2-3
이상이나 RHEL8에서 지원하지 않음으로 별도로 버전을 지정하여 설치합니다.$ yum -y install yum-utils +$ yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo +$ sudo yum repolist -v +... +Repo-id : docker-ce-stable +Repo-name : Docker CE Stable - x86_64 +Repo-revision: 1564098258 +Repo-updated : Fri 26 Jul 2019 08:44:18 AM KST +Repo-pkgs : 47 +Repo-size : 982 M +Repo-baseurl : https://download.docker.com/linux/centos/7/x86_64/stable +Repo-expire : 172,800 second(s) (last: Thu 25 Jul 2019 07:33:33 AM KST) +Repo-filename: /etc/yum.repos.d/docker-ce.repo +... + +$ yum -y install docker-ce-3:18.09.1-3.el7 +$ systemctl enable docker +$ systemctl start docker +
docker를 설치 한 뒤 API를 위한 TCP 포트를 활성화하는 작업을 진행합니다./lib/systemd/system/docker.service
에 ExecStart
옵션 뒤에 다음과 같이 -H tcp://0.0.0.0:4243
을 추가합니다.
...
+[Service]
+Type=notify
+# the default is not to use systemd for cgroups because the delegate issues still
+# exists and systemd currently does not support the cgroup feature set required
+# for containers run by docker
+ExecStart=/usr/bin/dockerd -H fd:// -H tcp://0.0.0.0:4243
+ExecReload=/bin/kill -s HUP $MAINPID
+TimeoutSec=0
+RestartSec=2
+Restart=always
+...
+
수정 후 서비스를 재시작합니다.
$ systemctl daemon-reload
+$ systemctl restart docker
+$ docker ps
+CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
+
+$ usermod -aG docker jenkins
+$ chmod 777 /var/run/docker.sock
+
Jenkins에 새로운 플러그인을 추가하고 설정합니다.
Jenkins 관리
로 이동하여 플러그인 관리
를 클릭합니다.
설치 가능
탭을 클릭하고 상단의 검색에 docker
를 입력하면 docker
플러그인이 나타납니다. 선택하여 설치를 진행하고 Jenkins를 재시작 합니다.
Jenkins 관리
로 이동하여 시스템 설정
을 클릭합니다.
Cloud
항목 아래 ADD A NEW CLOUD
드롭박스에 있는 Docker
를 선택합니다.
Name은 기본 값으로 진행하고 DOCKER CLOUD DETAILS...
버튼을 클릭합니다.
Docker Host URI : 앞서 설정한 port로 연결합니다. (e.g. tcp://master:4243)
TEST CONNECTION
버튼을 눌러 정상적으로 Version 정보와 API Version이 표기되는지 확인합니다.
Version = 18.09.1, API Version = 1.39
+
Enabled를 활성화 합니다.
Docker 실행을 위한 Item을 생성합니다. (e.g. 04-02.UsingDockerImagesForAgents)
Pipeline
스크립트를 구성합니다.pipeline {
+ agent {
+ docker { image 'node:latest' }
+ }
+ stages {
+ stage('Test'){
+ steps {
+ sh 'node --version'
+ }
+ }
+ }
+}
+
수정 후 좌측 Build Now
를 클릭하여 빌드 수행 후 결과를 확인합니다.
Step 1
에서의 결과와는 달리 Stage View
항목과 Pipeline stage가 수행된 결과를 확인할 수 있는 UI가 생성됩니다.
Step3. Configuring specific agents
Freestyle project
형태의 Item을 생성합니다. (e.g. 04-03.ConfiguringSpecificAgents)
Jenkins는 각 단계, 빌드, 그리고 빌드 후 작업일 지정할 수 있습니다.
Freestyle project
에서는 이같은 전체 빌드 단계를 구성하고 여러가지 플러그인을 사용할 수 있는 환경을 제공합니다.
General
Restrict where this project can be run : 빌드 수행을 특정 Label 노드에 제한하도록 설정할 수 있습니다.
Label Expression : 앞서의 과정에서 생성한 노드 Metal
을 지정해봅니다. 해당 조건의 노드가 존재하는 경우 노드 개수 정보가 표기됩니다.
Label Metal is serviced by 1 node.
+
Build
ADD BUILD STEP
드롭박스에서 Excute shell
항목을 선택하여 추가 합니다. echo "Hello world."
를 넣어봅니다.ADD BUILD STEP
드롭박스에서 Excute shell
항목을 선택하여 추가 합니다. ls -al"
를 넣어봅니다.저장하고 좌측의 Build Now
를 클릭하여 빌드를 수행합니다.
콘솔 출력을 확인하면 지정한 Label 노드에서 각 빌드 절차가 수행된 것을 확인할 수 있습니다.
Jenkins가 유용한 툴인 이유중 하나는 방대한 양의 플러그인 입니다. Jenkins의 기능을 확장시키고, 관리, 빌드 정책 등을 확장 시켜주고, 타 서비스와의 연계를 쉽게 가능하도록 합니다.
Step 1. Adding plugins via plugin manager
Jenkins는 온라인에 연결된 plugin을 검색, 설치할 수 있는 플러그인 관리
기능을 갖고 있습니다. 좌측 메뉴에서 Jenkins 관리
를 클릭하면 플러그인 관리
링크를 통하여 해당 기능에 접근할 수 있습니다.
.hpi
확장자를 갖는 플러그인을 설치하거나 업데이트 사이트를 지정할 수 있습니다.각 플러그인 이름을 클릭하면 플러그인 정보를 확인할 수 있는 plugins.jenkins.io
사이트로 이동하여 정보를 보여줍니다. 사용방법은 우측에 wiki
링크를 클릭합니다. 대략적인 UI나 사용방법은 wiki.jenkins.io
에서 제공합니다.
Step 2. Using shared libraries
Jenkins Pipeline의 Shared libraries에 대한 상세 내용은 다음 링크를 참고합니다. https://jenkins.io/doc/book/pipeline/shared-libraries/
이번 실습을 진행하기전에 GitHub에서 https://github.com/Great-Stone/evenOdd repository를 본인 계정의 GitHub에 Fork 하여 진행합니다.
소스의 var
디렉토리에는 Pipeline에서 사용하는 Shared Library들이 들어있습니다. groovy 스크립트로 되어있으며 Pipeline을 구성한 jenkinsfile
에서 이를 사용합니다.
vars/evenOdd.groovy
를 호출하고 값을 받아오는 형태를 갖고, evenOdd.groovy에서 사용하는 log.info
와 log.warning
은 vars/log.groovy
에 구현되어있습니다.
다음과 같이 Jenkins에 설정을 수행합니다.
Jenkins 관리
클릭 후 시스템 설정
을 선택합니다.Global Pipeline Libraries
의 추가 버튼을 클릭하여 새로운 구성을 추가합니다. Source Code Management
항목이 추가됩니다.GitHub
를 클릭하여 내용을 채웁니다. https://github.com/Great-Stone/evenOdd
인 경우 Great-Stone
이 Owner가 됩니다.Library
에 있는 Load implicitly
를 활성화 합니다.Shared Libraries가 준비가 되면 Pipeline
타입의 Item을 생성하고 (e.g. 05-02.UsingSharedLibraries) Pipeline 설정을 추가합니다.
저장 후 Build Now
를 클릭하여 빌드를 수행합니다. 빌드의 결과로는 2 단계로 수행되는데 1단계는 Declarative: Checkout SCM
으로 SCM으로부터 소스를 받아 준비하는 단계이고, 2단계는 jenkinsfile
을 수행하는 단계입니다. vars/evenOdd.goovy
스크립트에는 stage가 두개 있으나 해당 Pipeline 을 호출하는 값에 따라 하나의 stage만을 수행하도록 되어있어서 하나의 stage가 수행되었습니다.
// Jenkinsfile
+//@Library('evenOdd') _
+
+evenOdd(currentBuild.getNumber())
+
currentBuild.getNumber()
는 현재 생성된 Pipeline Item의 빌드 숫자에 따라 값을 evenOdd(빌드 숫자)
형태로 호출하게 됩니다.
Jenkins shared libraries를 사용하는 가장 좋은 예는 재사용성 있는 Groovy 함수를 타 작업자와 공유하는 것 입니다. 빌드의 상태는 다른 파이프 라인 단계로 계속할 것인지 결정하는 데 사용할 수도 있습니다.
주의
해당 설정은 모든 빌드에 영향을 주기 때문에 타 작업을 위해 추가된 Global Pipeline Libraries의 Library를 삭제하여 진행합니다.
Jenkins빌드의 결과를 받아볼 수 있는 몇가지 방안에 대해 알아봅니다.
Step 1. Notifications of build state
Jenkins에서는 플러그인이나 외부 툴에 의해 빌드에 대한 결과를 받아 볼 수 있습니다. 대표적으로는 Jenkins의 슬랙 플러그인을 사용하여 슬랙으로 빌드에 결과를 받아보거나, catlight.io 에서 데스크탑용 어플리케이션에 연동하는 방법도 있습니다.
여기서는 Chrome 확장 프로그램을 통한 알림을 받을 수 있는 설정을 설명합니다. 이 과정을 진행하기 위해서는 Chrome 웹브라우저가 필요합니다.
chrome://apps/에 접속하여 앱스토어를 클릭합니다.
jenkins를 검색하여 Yet Another Jenkins Notifier
를 확인합니다. Chrome에 추가
버튼으로 확장 프로그램을 설치합니다.
설치가 완료되면 브라우저 우측 상단에 Jenkins 아이콘이 나타납니다. 클릭합니다.
각 Item(Job)의 url을 입력하여 +
버튼을 클릭합니다.
등록된 Item을 확인하고 해당 빌드를 Jenkins 콘솔에서 실행해봅니다. 결과에 대한 알림이 발생하는 것을 확인 할 수 있습니다.
Step 2. Build state badges for SCM
Jenkins에서 빌드가 수행된 결과를 SCM에 반영하는 기능도 플러그인을 통해 가능합니다. SCM에서 해당 Jenkins에 접근이 가능해야 하므로 Jenkins는 SCM에서 접근가능한 네트워크 상태여야 합니다.
Jenkins에 새로운 플러그인을 추가하고 설정합니다.
Jenkins 관리
로 이동하여 플러그인 관리
를 클릭합니다.설치 가능
탭을 클릭하고 상단의 검색에 embed
를 입력하면 Embeddable Build Status
플러그인이 나타납니다. 선택하여 설치를 진행합니다.Jenkins 관리
로 이동하여 Configure Global Security
을 클릭합니다.Authorization
항목에서 Matrix-based security
를 체크합니다.Authenticated Users
의 경우 필요한 각 항목에 대해 체크박스를 활성화 합니다.Anonymous Users
에 대해서 Job/ViewStatus
항목을 활성화 합니다.이제 기존의 외부 SCM이 연결된 Item을 선택합니다. 여기서는 05-02.UsingSharedLibraries
에 설정합니다. 해당 Item을 선택하면 좌측에 Embeddable Build Status
항목이 새로 생긴것을 확인 할 수 있습니다.
해당 항목을 클릭하고 Markdown
의 unprotected
의 항목을 복사합니다.
[![Build Status](http://myjenkins.com/buildStatus/icon?job=05-02.UsingSharedLibraries)](http://myjenkins.com/job/05-02.UsingSharedLibraries/)
+
복사한 형식을 GitHub의 evenOdd repository의 README.md 파일 상단에 위치 시킵니다.
# evenOdd
+[![Build Status](http://myjenkins.com/buildStatus/icon?job=libraries)](http://myjenkins.com/job/libraries/)
+
+A Jenkins even/odd playbook from the Jenkins.io documentation
+
+Add this as a shared library called evenOdd in your jenkins
+instance, and then instantiate the pipeline in your project Jenkinsfile
+
+This will also use an example of global variabls from the log.groovy
+definitions
+
+
이같이 반영하면 각 빌드에 대한 결과를 SCM에 동적으로 상태를 반영 할 수 있습니다.
이같은 알림 설정은 코드의 빌드가 얼마나 잘 수행되는지 이해하고 추적할 수 있도록 도와줍니다.
Step 1. Code coverage tests and reports
테스트 Pipeline 구성시 테스트 과정을 지정할 수 있습니다. Testing을 위한 Pipeline
타입의 Item을 추가로 생성합니다. (e.g. 07-01.CodeCoverageTestsAndReports)
설정은 다음과 같이 수행합니다.
Pipeline
스크립트에 다음과 같이 입력 합니다. 테스트와 빌드, 검증 후 결과를 보관하는 단계까지 이루어 집니다.
pipeline {
+ agent any
+ stages {
+ stage('Build') {
+ steps {
+ sh '''
+ echo This > app.sh
+ echo That >> app.sh
+ '''
+ }
+ }
+ stage('Test') {
+ steps {
+ sh '''
+ grep This app.sh >> ${BUILD_ID}.cov
+ grep That app.sh >> ${BUILD_ID}.cov
+ '''
+ }
+ }
+ stage('Coverage'){
+ steps {
+ sh '''
+ app_lines=`cat app.sh | wc -l`
+ cov_lines=`cat ${BUILD_ID}.cov | wc -l`
+ echo The app has `expr $app_lines - $cov_lines` lines uncovered > ${BUILD_ID}.rpt
+ cat ${BUILD_ID}.rpt
+ '''
+ archiveArtifacts "${env.BUILD_ID}.rpt"
+ }
+ }
+ }
+}
+
빌드가 완료되면 해당 Job화면을 리로드 합니다. Pipeline에 archiveArtifacts
가 추가되었으므로 해당 Job에서 이를 관리합니다.
해당 아카이브에는 코드 검증 후의 결과가 저장 됩니다.
Step 2. Using test results to stop the build
테스트 결과에 따라 빌드를 중지시키는 Pipeline 스크립트를 확인합니다. Testing을 위한 Pipeline
타입의 Item을 추가로 생성합니다. (e.g. 07-02.UsingTestResultsToStopTheBuild)
설정은 다음과 같이 수행합니다.
Pipeline
스크립트에 다음과 같이 입력 합니다. 테스트와 빌드, 검증 후 결과를 보관하는 단계까지 이루어 집니다.
pipeline {
+ agent any
+ stages {
+ stage('Build') {
+ steps {
+ sh '''
+ echo This > app.sh
+ echo That >> app.sh
+ echo The Other >> app.sh
+ '''
+ }
+ }
+ stage('Test') {
+ steps {
+ sh '''
+ for n in This That Those
+ do if grep $n app.sh >> ${BUILD_ID}.cov
+ then exit 1
+ fi
+ done
+ '''
+ }
+ }
+ stage('Coverage'){
+ steps {
+ sh '''
+ app_lines=`cat app.sh | wc -l`
+ cov_lines=`cat ${BUILD_ID}.cov | wc -l`
+ echo The app has `expr $app_lines - $cov_lines` lines uncovered > ${BUILD_ID}.rpt
+ cat ${BUILD_ID}.rpt
+ '''
+ archiveArtifacts "${env.BUILD_ID}.rpt"
+ }
+ }
+ }
+}
+
저장을 하고 빌드를 수행하면, Pipeline 스크립트 상 Test
Stage에서 조건 만족 시 exit 1
를 수행하므로 빌드는 중간에 멈추게 됩니다.
Jenkins는 외부 서비스와의 연동이나 정보 조회를 위한 API를 제공합니다.
Step 1. Triggering builds via the REST API
Jenkins REST API 테스트를 위해서는 Jenkins에 인증 가능한 Token을 취득하고 curl이나 Postman 같은 도구를 사용하여 확인 가능 합니다. 우선 Token을 얻는 방법은 다음과 같습니다.
Jenkins에 로그인 합니다.
우측 상단의 로그인 아이디에 마우스를 호버하면 드롭박스 버튼이 나타납니다. 설정
을 클릭합니다.
API Token
에서 Current token
을 확인합니다. 등록된 Token이 없는 경우 다음과 같이 신규 Token을 발급 받습니다.
ADD NEW TOKEN
을 클릭합니다.
이름을 기입하는 칸에 로그인 한 아이디를 등록합니다. (e.g. admin)
GENERATE
를 클릭하여 Token을 생성합니다.
이름과 Token을 사용하여 다음과 같이 curl로 접속하면 Jenkins-Crumb
프롬프트가 나타납니다.
$ curl --user "admin:TOKEN" 'http://myjenkins.com/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,":",//crumb)'
+
+Jenkins-Crumb:89e1fd9c402824c89465f6b97f49b605
+
Crumb
를 확인했으면 다시 헤더 값에 Jenkins-Crumb:
를 추가하여 02-04.MultiStep
Job을 빌드하기 위해 다음과 같이 요청합니다.
$ curl -X POST http://myjenkins.com/job/02-04.MultiStep/build --user gyulee:11479bdec9cada082d189938a3946348be --data-urlencode json='' -H "Jenkins-Crumb:89e1fd9c402824c89465f6b97f49b605"
+
API로 호출된 빌드가 수행되어 빌드 번호가 증가하는 것을 확인합니다.
Step 2. Retriving build status via the REST API
빌드에 대한 결과를 REST API를 통해 요청하는 방법을 알아봅니다. 앞서 진행시의 Token값이 필요합니다. Json 형태로 출력되기 때문에 정렬을 위해 python이 설치 되어있다면 mjson.tool
을 사용하여 보기 좋은 형태로 출력 가능합니다.
# Python이 설치되어있지 않은 경우
+$ yum -y install python2
+
+# Jenkins에 REST API로 마지막 빌드 상태 요청
+$ curl -s --user gyulee:11479bdec9cada082d189938a3946348be http://myjenkins.com/job/02-04.MultiStep/lastBuild/api/json | python2 -mjson.tool
+
+{
+ "_class": "org.jenkinsci.plugins.workflow.job.WorkflowRun",
+ "actions": [
+ {
+ "_class": "hudson.model.CauseAction",
+ "causes": [
+ {
+ "_class": "hudson.model.Cause$UserIdCause",
+ "shortDescription": "Started by user GyuSeok.Lee",
+ "userId": "gyulee",
+ "userName": "GyuSeok.Lee"
+ }
+ ]
+ },
+ {},
+ {
+ "_class": "hudson.plugins.git.util.BuildData",
+ "buildsByBranchName": {
+ "master": {
+ "_class": "hudson.plugins.git.util.Build",
+ "buildNumber": 5,
+ "buildResult": null,
+...
+
Step 1. Securing your deployment with users
사용자별 배포수행을 위한 사용자 설정을 설명합니다.
Jenkins 관리
로 이동하여 Configure Global Security
를 클릭합니다.Enable security
는 보안 설정 여부를 설정하는 항목으로 기본적으로는 비활성화되어있습니다. 체크하여 활성화하면 다양한 보안 옵션을 설정할 수 있는 항목이 표기 됩니다.
Security Realm 에서는 Jenkins에서 사용하는 사용자 관리 방식을 선택합니다.
사용자의 가입 허용
이 활성화되면 Jenkins 에 접속하는 사용자는 스스로 계정을 생성하고 접근 가능합니다.Authorization 에서는 사용자 권한에 대한 설정을 정의합니다.
1.164
이전 버전의 동작과 동일하게 관리됩니다. Admin
사용자만 모든 기능을 수행하며, 일반 사용자와 비로그인 사용자는 읽기만 가능합니다.다음은 권한 매트릭스의 항목과 권한별 설명입니다.
항목 | 권한 | 의미 |
---|---|---|
Overall | Administer | 시스템의 전역 설정을 변경할 수 있다. OS 에서 허용된 범위안에서 전체 시스템 엑세스드의 매우 민감한 설정을 수행 |
Read | 젠킨스의 모든 페이지 확인 가능 | |
RunScripts | 그루비 콘솔이나 그루비 CLI 명령을 통해 그루비 스크립트를 실행 | |
UploadPlugins | 특정 플러그인을 업로드 | |
ConfigureUpdateCenter | 업데이트 사이트와 프록시 설정 | |
Slave | Configure | 기존 슬레이브 설정 가능 |
Delete | 기존 슬레이브 삭제 | |
Create | 신규 슬레이브 생성 | |
Disconnect | 슬레이브 연결을 끊거나 슬레이브를 임시로 오프라인으로 표시 | |
Connect | 슬레이브와 연결하거나 슬레이브를 온라인으로 표시 | |
Job | Create | 새로운 작업 생성 |
Delete | 기존 작업 삭제 | |
Configure | 기존 작업의 설정 갱신 | |
Read | 프로젝트 설정에 읽기 전용 권한 부여 | |
Discover | 익명 사용자가 작업을 볼 권한이 없으면 에러 메시지 표시를 하지 않고 로그인 폼으로 전환 | |
Build | 새로운 빌드 시작 | |
Workspace | 젠킨스 빌드를 실행 하기 위해 체크아웃 한 작업 영역의 내용을 가져오기 가능 | |
Cancel | 실행중인 빌드 취소 | |
Run | Delete | 빌드 내역에서 특정 빌드 삭제 |
Update | 빌드의 설명과 기타 프로퍼티 수정(빌드 실패 사유등) | |
View | Create | 새로운 뷰 생성 |
Delete | 기존 뷰 삭제 | |
Configure | 기존 뷰 설정 갱신 | |
Read | 기존 뷰 보기 | |
SCM | Tag | 특정 빌드와 관련된 소스 관리 시스템에 태깅을 생성 |
CSRF Protection 항목에 있는 Prevent Cross Site Request Forgery exploits
항목은 페이지마다 nonce 또는 crumb 이라 불리우는 임시 값을 삽입하여 사이트 간 요청 위조 공격을 막을 수 있게 해줍니다. 사용방법은 위에서 REST API 에 대한 설명 시 crumb 값을 얻고, 사용하는 방법을 참고합니다.
Step 2. Securing secret credentials and files
Jenkins에서 Pipeline을 설정하는 경우 일부 보안적인 값이 필요한 경우가 있습니다. 예를 들면 Username
과 Password
같은 값입니다. 앞서의 과정에서 Credentials
를 생성하는 작업을 일부 수행해 보았습니다. 여기서는 생성된 인증 값을 Pipeline에 적용하는 방법을 설명합니다.
Pipeline
타입의 Item을 추가로 생성합니다. (e.g. 09-02.SecuringSecretCredentialsAndFiles) 설정은 다음과 같이 수행합니다.
Pipeline
스크립트에 다음과 같이 입력 합니다.
pipeline {
+ agent any
+ environment {
+ SECRET=credentials('jenkins-secret-text')
+ }
+ stages {
+ stage('Build') {
+ steps {
+ echo "${env.SECRET}"
+ }
+ }
+ }
+}
+
저장 후 Build Now
를 클릭하여 빌드를 수행하면 실패하게 되고 Console Output
에서 진행사항을 보면, Pipeline 스크립트에서 선언한 jenkins-secret-text
때문에 에러가 발생한 것을 확인할 수 있습니다.
좌측 상단의 Jenkins
버튼을 클릭하여 최상위 메뉴로 이동합니다.
좌측 메뉴의 Credentials
를 클릭하고 (global)
도메인을 클릭합니다.
좌측에 Add Credentials
를 클릭하여 새로운 항목을 추가합니다.
저장 후 다시 빌드를 수행하면 정상적으로 수행됩니다. 해당 값은 숨기기 위한 값이므로 Pipeline 스크립트에서 echo
로 호출하더라도 ****
이란 값으로 표기 됩니다.
이같은 방법은 Password같은 보안에 민감한 정보를 사용하기에 유용합니다.
Step 3. Auditing your environment
Jenkins의 변화와 활동에 대한 감시를 위한 설정 방법을 설명합니다. Jenkins에 새로운 플러그인을 추가하고 설정합니다.
Jenkins 관리
로 이동하여 플러그인 관리
를 클릭합니다.설치 가능
탭을 클릭하고 상단의 검색에 audit
를 입력하면 Audit Trail
플러그인이 나타납니다. 선택하여 설치합니다.Jenkins 관리
로 이동하여 시스템 설정
을 클릭합니다.ADD LOGGER
드롭박스에서 Log File
을 선택하여 설정합니다. 저장 후 빌드나 Job의 설정 변경등의 작업을 수행하면, audit.log.0
으로 지정된 파일 경로에 생성됨을 확인 할 수 있습니다.
$ tail -f ./audit.log.0
+Jul 31, 2019 10:47:32,727 AM job/02-02.Jobs/ #12 Started by user GyuSeok.Lee
+Jul 31, 2019 10:47:42,738 AM /job/03-04.WebhookBuild Triggering/configSubmit by gyulee
+Jul 31, 2019 10:48:09,001 AM /configSubmit by gyulee
+
Step 4. Using forders to create security realms
다양한 프로젝트를 관리하는 경우 관리상, 빌드 프로젝트를 관리해야할 필요성이 발생합니다. Jenkins에서 Forder 아이템을 생성하여 관리 편의성과 보안요소를 추가할 수 있습니다.
우선 테스트를 위한 사용자를 추가합니다.
Jenkins 관리
를 클릭하여 Manage Users
로 이동합니다.사용자 생성
을 클릭하여 새로운 사용자를 추가합니다. 다음으로 Forder 타임의 Item을 추가합니다.
새로운 Item
을 클릭하여 이름을 02-Project
로 예를 들어 지정하고, Forder를 클릭하여 OK
버튼을 클릭합니다.SAVE
버튼을 클릭하고 좌측 상단의 Jenkins
버튼을 클릭하여 최상위 페이지로 이동합니다.02-02.Jobs
에 마우스를 대면 드롭박스 메뉴를 확장할 수 있습니다. Move
를 클릭합니다.Jenkins >> 02-Project
를 선택하고 MOVE
버튼을 클릭합니다. 다시 최상위 메뉴로 오면 02-02.Jobs
가 사라진 것을 확인할 수 있습니다. 02
로 시작하는 다은 프로젝트도 같은 작업을 수행하여 이동시킵니다.02-Project
를 클릭하면 이동된 프로젝트들이 나타납니다.권한 설정을 하여 현재 Admin 권한의 사용자는 접근 가능하고 새로 생성한 tester는 접근불가하도록 설정합니다.
Folder에 접근하는 권한을 설정하기위해 Jenkins 관리
의 Configure Global Security
로 이동합니다.
Authorization항목의 Project-based Matrix Authorization Strategy
를 선택합니다.
ADD USER OR GROUP...
을 클릭하여 Admin 권한의 사용자를 추가합니다.
Admin 권한의 사용자에게는 모든 권한을 주고 Authenticated Users
에는 Overall의 Read
권한만 부여합니다.
생성한 02-Project
로 이동하여 좌측 메뉴의 Configure
를 클릭합니다.
Properties에 추가된 Enable project-based security
를 확성화하면 항목별 권한 관리 메트릭스가 표시됩니다. Job의 Build, Read, ViewStatus, Workspace를 클릭하고 View의 Read를 클릭하여 권한을 부여합니다.
로그아웃 후에 앞서 추가한 test
사용자로 로그인 하면 기본적으로 다른 프로젝트나 Item들은 권한이 없기 때문에 보이지 않고, 앞서 설정한 02-Project
폴더만 리스트에 나타납니다.
Jenkins의 인증 기능을 사용하여 보안적 요소를 구성할 수 있습니다. Audit 로그를 활용하여 사용자별 활동을 기록할 수도 있고 Folder를 활용하면 간단히 사용자/그룹에 프로젝트를 구분하여 사용할 수 있도록 구성할 수 있습니다.
빌드 이후 빌드의 결과를 기록하고 저장하는 방법을 설명합니다.
Step 1. Creating and storing artifacts
Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 10-01.CreatingAndStoringArtifacts)
Pipeline에 다음과 같이 스크립트를 추가합니다.
pipeline {
+ agent any
+ stages{
+ stage('Build') {
+ steps{
+ sh 'echo "Generating artifacts for ${BUILD_NUMBER}" > output.txt'
+ }
+ }
+ stage('Archive') {
+ steps {
+ archiveArtifacts artifacts: 'output.txt', onlyIfSuccessful: true
+ }
+ }
+ }
+}
+
Archive
Stage에 archiveArtifacts
스크립트가 동작하는 예제입니다. 이같은 Pipeline 스크립트 작성을 도와주는 툴을 추가로 확인해 봅니다.
Pipeline Syntax
링크를 클릭합니다.Sample Step
에서 archiveArtifacts: Archive the artifacts
를 선택합니다. 고급...
을 클릭합니다.GENERATE PIPELINE SCRIPT
를 클릭합니다.결과물을 확인하면 Pipeline 스크립트에 작성한 형태와 같은 것을 확인 할 수 있습니다.
좌측 메뉴의 Build Now
를 클릭하여 빌드 수행 후에 화면에 Artifacts 항목이 추가된 것을 확인할 수 있습니다. UI 상에는 마지막 빌드 결과가 강조되어 나오고 각 빌드에 대한 결과물은 각각의 빌드단계의 다운로드 버튼으로 확인하고 다운로드 할 수 있습니다.
Step 2. Fingerprinting for artifact tracking
빌드 이후 보관되는 파일에 대해 어떤 프로젝트, 어떤 빌드 에서 발생한 결과물인지 확인할 수 있는 핑거프린팅 기능을 설명합니다.
Step 1
의 프로젝트를 그대로 사용하거나 Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 10-02.FingerprintingForArtifactTracking)
Step 1
Pipeline 스크립트의 archiveArtifacts
에 fingerprint: true
를 추가합니다.
pipeline {
+ agent any
+ stages{
+ stage('Build') {
+ steps{
+ sh 'echo "Generating text artifacts: Build:${BUILD_NUMBER}" > output.txt'
+ }
+ }
+ stage('Archive') {
+ steps {
+ archiveArtifacts artifacts: 'output.txt', fingerprint: true, onlyIfSuccessful: true
+ }
+ }
+ }
+}
+
파일의 지문을 확인합니다.
첫번째 빌드를 수행하고 빌드 결과 아카이브 파일 output.txt
파일을 다운로드 받습니다. (파일을 우클릭하고 다른 이름으로 링크 저장...
or Download Linked File
을 클릭하여 파일을 받습니다.)
좌측 상단의 Jenkins
를 클릭하여 최상위 메뉴로 돌아갑니다.
좌측 메뉴의 파일 핑거프린트 확인
을 클릭합니다.
파일 선택
버튼을 클릭하여 앞서 다운로드한 파일을 선택하고 확인하기
버튼을 클릭합니다.
어떤 프로젝트의 몇번째 빌드에서 발생한 파일인지 확인합니다.
두번째 빌드를 수행하고 파일 핑거프린트를 확인해 봅니다.
빌드 번호 정보가 변경된 것을 확인합니다.
Pipeline에 대해 설명합니다.
Step 1. Automating deployment with pipelines
Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 11-01.AutomatingDeploymentWithPipelines)
Pipeline에 다음과 같은 스크립트를 입력합니다.
pipeline {
+ agent any
+ stages {
+ stage('Build') {
+ steps {
+ sh 'echo "Hello World"'
+ }
+ }
+ stage('Test') {
+ steps {
+ sh 'echo "Test Hello World!"'
+ }
+ }
+ }
+}
+
두개의 Stage를 갖는 Pipeline 스크립트입니다. Pipeline은 빌드 수행시의 각 단계를 구분하여 빌드의 과정을 확인하고 실패에 따른 단계별 확인이 가능합니다.
좌측 Build Now
를 클릭하여 빌드를 수행하면 빌드에 대한 결과는 Stage 별로 성공 실패의 여부와 로그를 확인할 수 있도록 Stage View
가 UI로 제공됩니다. Stage 별로 Stage View는 기록되며, Stage에 변경이 있거나 이름이 변경되는 경우에는 해당 UI에 변경이 발생하여 기존 Pipeline 기록을 보지 못할 수 있습니다.
Step 2. Creating pipeline gates
Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 11-02.CreatingPipelineGates)
Pipeline에 다음과 같은 스크립트를 입력합니다.
pipeline {
+ agent any
+ stages {
+ stage('Build') {
+ steps {
+ sh 'echo "Hello World"'
+ }
+ }
+ stage('BuildMore'){
+ steps {
+ input message: "Shall we build more?"
+ sh '''
+ echo "We are approved; continue!"
+ ls -lah
+ '''
+ }
+ }
+ }
+}
+
개의 Stage를 갖는 Pipeline 스크립트입니다. 두번째 Stage에 input
스크립트가 있습니다. 이 스크립트가 추가되면 Pipeline을 진행하면서 해당하는 동작을 수행할 것인지, 마치 승인 작업과 같은 동작을 수행할 수 있습니다.
좌측 Build Now
를 클릭하여 빌드를 수행하면 두번째 Stage에서 해당 작업을 수행할 지에 대한 물음을 확인 할 수 있습니다.
Abort
를 선택하면 빌드 취소와 같은 동작으로 실패로 처리되지는 않습니다.
Step 3. Job promotion for long-running pipeline
빌드 단계를 구현할 때 Pipeline 스크립트로 하나의 프로젝트 내에서 모든 동작을 정의 할 수도 있지만 서로다른 Job을 연계하고, 승인 절차를 따르도록 구성할 수 있습니다.
Job promotion 기능을 사용하기 위한 플러그인을 설치합니다.
Jenkins 관리
에서 플러그인 관리
를 선택합니다.설치 가능
탭을 클릭하고 상단의 검색에 promoted
를 입력하면 promoted builds
를 확인 할 수 있습니다. 설치합니다.FreeStyle 타입의 Item을 생성합니다. (e.g. 11-03.Job-one)
General 탭의 Promote builds when...
를 활성화 하여 설정합니다.
Only when manually approved
활성화 ADD PRAMETER
드롭박스에서 Boolean Parameter
를 선택합니다. Build 드롭박스에서 Execute shell
을 선택합니다.
다음을 입력합니다.
echo 'This is the Job-one'
+
저장하면 생성된 프로젝트에 Promotion Status
항목이 추가되어 생성됩니다.
11-03.Job-one
빌드 후 승인에 대한 다음 빌드를 진행할 FreeStyle 타입의 Item을 생성합니다. (e.g. 11-03.Job-two)
빌드 유발 항목에서 Build when another project is promoted
를 활성화 합니다. 어떤 Job에서 promote 상황이 발생하였을 때 빌드를 수행할지 지정합니다.
Build 드롭박스에서 Execute shell
을 선택합니다.
다음을 입력합니다.
echo 'This is the Job-two'
+
11-03.Job-one
에 대한 빌드를 수행합니다. 수행 완료 후 빌드 히스토리의 최근 빌드를 클릭(e.g. #1)하면 Promotion Status
에 승인절차를 기다리고 있음을 확인할 수 있습니다. Parameters 항목의 approve
를 체크하고 APPROVE
버튼을 클릭합니다.
승인이 완료되면 해당 프로젝트의 승인에 대한 이벤트를 통해 빌드를 수행하는 11-03.Job-two
가 이어서 빌드됨을 확인 할 수 있습니다.
Step 4. Multibranch repository automation
SCM의 Multibranch를 빌드하는 과정에 대해 설명합니다.
다음의 GitHub repository를 fork 합니다.
Multibranch Pipeline 형태의 Item을 생성합니다. (e.g. 11-04.MultibranchRepositoryAutomation)
ADD SOURCE
드롭박스에서 GitHub를 클릭합니다. VALIDATE
버튼을 클릭하여 잘 접근 되는지 확인합니다.Periodically if not otherwise run
를 활성화 합니다. 1 minute
으로 설정합니다.저장 후에는 자동적으로 모든 브랜치의 소스를 빌드 수행합니다.
SCM에서 브랜치를 여러개 관리하고 모두 빌드와 테스팅이 필요하다면 Multibranch 프로젝트를 생성하여 등록하고, 빌드 관리가 가능합니다.
Step 5. Creating pipeline with snippets
Pipeline 을 스크립트를 작성하는 방법을 배워봅니다. Pipeline 타입의 Item을 생성합니다. (e.g. 11-05. CreatingPipelineWithSnippets)
Pipeline에 다음과 같은 스크립트를 입력합니다.
pipeline {
+ agent any
+ stages {
+ stage("Hello") {
+ steps {
+ echo 'Hello World'
+ }
+ }
+ }
+}
+
echo가 동작할때 시간을 기록하도록 스크립트를 수정해보겠습니다.
Pipeline Syntax 링크를 클릭합니다.
Sample Step에서 timestamps: timestamps
를 선택하고 GENERATE PIPELINE SCRIPT
버튼을 클릭합니다.
timestamps {
+ // some block
+}
+
사용방식을 확인하고 앞서 Pipeline 스크립트의 stage에 시간을 기록하도록 수정합니다.
...
+stage("Hello") {
+ steps {
+ timestamps {
+ echo 'Hello World'
+ }
+ }
+}
+...
+
빌드를 수행하고 로그를 확인해 봅니다. echo 동작이 수행 될때 시간이 함께 표기되는 것을 확인 할 수 있습니다.
Step 6. Discovering global pipeline variables
Pipeline에서 사용할 수 있는 변수를 확인하고 사용하는 방법을 알아봅니다. Pipeline 타입의 Item을 생성합니다. (e.g. 11-06.DiscoveringGlobalPipelineVariables)
Pipeline에 다음과 같은 스크립트를 입력합니다.
pipeline {
+ agent any
+ stages {
+ stage('Build') {
+ steps {
+ echo "We are in build ${currentBuild.number}"
+ echo "Our current result is ${currentBuild.currentResult}"
+ }
+ }
+ stage('BuildMore'){
+ steps {
+ echo "Name of the project is ${currentBuild.projectName}"
+ }
+ }
+ stage('BuildEnv'){
+ steps {
+ echo "Jenkins Home : ${env.JENKINS_HOME}"
+ }
+ }
+ }
+}
+
Pipeline 스크립트에서 사용가능한 변수와 사용방법은 Pipeline Syntax
링크의 Global Variables Reference
항목에서 확인 가능합니다.
GitHub SCM 연동 이슈
GitHub를 SCM으로 사용하는 경우 다음과 같은 메시지가 출력되면서 진행되지 않는 경우가 있습니다.
GitHub API Usage: Current quota has 5000 remaining (447380 over budget). Next quota of 5000 in 5 days 0 hr. Sleeping for 4 days 23 hr.
+14:07:33 GitHub API Usage: The quota may have been refreshed earlier than expected, rechecking...
+
이 경우 서버 시간과 GitHub의 시간이 맞지 않아 발생할 수 있는 이슈 입니다. ntpdate를 재설정 합니다.
RHEL7 : ntpd를 재시작 합니다.
$ systemctl restart ntpd
+
RHEL8 : RHEL8에서는 ntpdate를 사용하지 않고 chronyd가 대신합니다.
https://access.redhat.com/solutions/4130881
$ systemctl stop chronyd
+$ chronyd -q
+$ systemctl start chronyd
+
유용한 플러그인
본 내용은 톰캣을 좀더 잘 알고 잘 써보기 위한 제안이랄까요?
톰캣의 특성상 쉽게 접할 수 있는 메뉴얼적인 지식보다는, 톰캣을 더 잘 사용하고 운영 할 수 있을만한 아이디어를 공유하고자 시작한 지식공유 활동입니다. 담고 있는 내용은 '톰캣 알고 쓰기' 유튜브 강의 내용에 대한 정리입니다. 유튜브에 강의를 올리면 출퇴근 시간을 이용해 짬짬히 들을 수 있을 것 같은 생각이 들어 시작하였지만 얼마나 출퇴근 시간에 이용하셨을지는 미지수이고 동영상으로 모든 것을 다 표현할 수 없다는 점을 감안하여 다시 글로 정리합니다.
톰캣을 잘 사용하고 이해하는 것은 개발자, 운영자, 기술자, 엔지니어등 톰캣을 접하는 모두에게 요구되는 사항이지만 일반적으로는 필요한 기능과 기술만을 습득하게 됩니다. 이번 강의를 통해 약간의 시간 투자로 모두에게 도움이 되었으면 합니다.
특히 WEB/WAS 엔지니어로 자주 겪었던 개발은 톰캣으로하고 정작 운영은 다른 WAS를 사용하는 아이러니함에도 도움이 되었으면 합니다.
톰캣을 사용하는 첫번째 이유는 기능적인 이유일 것입니다. 즉, 톰캣이 수행하는 역할인 JSP/Servlet 엔진으로서의 역할이겠지요. 톰캣은 세계적으로 가장 많은 Java 기반의 웹어플리케이션 플랫폼으로 사용되고 있습니다. 많은 개발자들이 자신의 첫번째 Java 웹 어플리케이션의 JSP/Servlet 엔진으로 선택하고 있고 실제 운영환경에서도 사용하기에 상당한 양의 노하우를 쉽게 접할 수 있다는 장점이 있습니다.
두번째로는 아마도 무료로 사용할 수 있다는 점이 톰캣을 사용하게 되는 이유일 것입니다. 수많은 벤더사에서 JSP/Servlet 엔진과 추가로 Java Enterprise 기능을 사용할 수 있는 세련되고 검증된 자신만의 제품을 개발하고 상용화 하고 있습니다. 하지만 이러한 제품은 비용이 추가된다는 (큰)고민이 생깁니다. 물론 유지보수 계약을 통해 기술지원과 더불어 든든한 책임전가의 대상(?)인 벤더사가 존재한다는 장점이 있지만 모든 사용자가 이런 비용을 지불할 수 있는것은 아니겠지요.
2020년 JRebel 자료
Java 웹 어플리케이션을 실행하는 Application Server의 종류는 거의 30가지에 달하나 조사된 JRebel의 통계에 따르면 여전히 과반 이상을 Tomcat이 점유하고 있다고 합니다. 여러가지 이유가 더 있겠지만 앞서 언급한 많은 레퍼런스와 무료라는 큰 특징으로 인해 수많은 사용자가 톰캣을 사용하고 있습니다.
JRebel에서 언급한것과 같이 Spring Boot, Docker, Hybris 및 AWS와 같은 다른 주요 Java 플랫폼과의 호환성이 뒷받침이 되는것 같습니다.
톰캣의 정식 명칭은 Apache Tomcat Server 입니다. 이를 줄여 톰캣 Tomcat 이라고 흔히 불려지고 있지요. 톰캣의 간단한 이력은 다음과 같습니다.
기준 | 2021년 12월 23일 |
---|---|
Deveoloper | Apache Software Foundation |
Last Stable release | 10.0.14 |
Development Status | Active |
Written in | Java |
Operating System | Cross-Platform |
Type | Servlet Container / HTTP Web Server |
License | Apache License 2.0 |
Website | tomcat.apache.org |
현재까지 출시된 버전은 10.0.14 버전이고 앞으로도 지속적으로 업데이트가 있을 예정입니다. 무료로 제공되는 이유로 인해 톰캣을 기반으로 한 소프트웨어들이 있는데, 이경우 개발된 시점을 기준으로 톰캣 버전이 고정되어 있는 경우가 있습니다. 4.0 버전은 최근 보이지 않지만 JDK 1.4.2 를 사용하던 시기가 가장 많은 Java 웹 어플리케이션이 개발되던 시기이기에 톰캣 5.5 버전은 아직도 상당히 많은 사용처가 있을것이라 보여집니다.
톰캣을 구성하는 핵심적인 요소는 다음의 세가지 컴포넌트입니다.
Catalina는 아마도 톰캣을 사용하면 가장 많이 보게되는 단어 중 하나일 것입니다.
처리되는 컴포넌트의 역할을 이해한다면 톰캣에서 어플리케이션 수행시 발생되는 코드 스텍을 이해하는데 도움이 될 수 있습니다.
톰캣에 대하여 흔히들 '톰캣은 완전한 WAS(Web Application Server)가 아니다'라고 합니다. 앞서 설명하였지만 톰캣은 JSP/Servlet 엔진의 역할을 수행합니다. 하지만 Java Enterprise 기능인 EJB, JTA, JMS, WebService 등은 포함되어 있지 않죠. 이러한 이유로 WAS의 일부 기능만을 수행 할 수 있을 뿐 WAS는 아니다 라고 합니다. 이러한 Enterprise 요소를 지원하기위한 요구사항으로 OpenEJB나 Apache ActiveMQ, Apache CXF등의 컴포넌트 요소가 톰캣과는 별도의 프로젝트로 진행되어 해당 컴포넌트들을 결합함으로 톰캣에서 이를 지원할 수 있었습니다. 이제는 어느정도 시간이 지나 Enterprise 컴포넌트와의 연계성이 뚜렷해졌고 통합이 가능해짐에 따라 톰캣을 Java Enterprise 스펙에 맞게 재 조정하는 프로젝트가 시작됩니다. 이를 TomEE(Tomcat Enterprise Edition)이라 합니다.
TomEE의 대표 홈페이지는 tomee.apache.org이며 상세한 설명과 문서가 준비되어있기 때문에 사용하는데 큰 어려움이 없습니다. TomEE에서 지원하는 Java Enterprise 컴포넌트들은 다음과 같습니다.
https://tomee.apache.org/comparison.html
Tomcat | TomEE WebProfile | TomEE MicroProfile | TomEE Plume | TomEE Plus | |
---|---|---|---|---|---|
Jakarta Annotations | ✔︎ | ✔︎ | ✔︎ | ✔︎ | ✔︎ |
Jakarta Debugging Support for Other Languages | ✔︎ | ✔︎ | ✔︎ | ✔︎ | ✔︎ |
Jakarta Security (Java EE Enterprise Security) | ✔︎ | ✔︎ | ✔︎ | ✔︎ | ✔︎ |
Jakarta Server Pages (JSP) | ✔︎ | ✔︎ | ✔︎ | ✔︎ | ✔︎ |
Jakarta Servlet | ✔︎ | ✔︎ | ✔︎ | ✔︎ | ✔︎ |
Jakarta Standard Tag Library (JSTL) | ✔︎ | ✔︎ | ✔︎ | ✔︎ | ✔︎ |
Jakarta WebSocket | ✔︎ | ✔︎ | ✔︎ | ✔︎ | ✔︎ |
Jakarta Expression Language (EL) | ✔︎ | ✔︎ | ✔︎ | ✔︎ | ✔︎ |
Jakarta Activation | ✔︎ | ✔︎ | ✔︎ | ✔︎ | |
Jakarta Bean Validation | ✔︎ | ✔︎ | ✔︎ | ✔︎ | |
Jakarta Contexts and Dependency Injection (CDI) | ✔︎ | ✔︎ | ✔︎ | ✔︎ | |
Jakarta Dependency Injection (@Inject) | ✔︎ | ✔︎ | ✔︎ | ✔︎ | |
Jakarta Enterprise Beans (EJB) | ✔︎ | ✔︎ | ✔︎ | ✔︎ | |
Jakarta Interceptors | ✔︎ | ✔︎ | ✔︎ | ✔︎ | |
Jakarta JSON Binding (JSON-B) | ✔︎ | ✔︎ | ✔︎ | ✔︎ | |
Jakarta JSON Processing (JSON-P) | ✔︎ | ✔︎ | ✔︎ | ✔︎ | |
Jakarta Mail (JavaMail) | ✔︎ | ✔︎ | ✔︎ | ✔︎ | |
Jakarta Managed Beans | ✔︎ | ✔︎ | ✔︎ | ✔︎ | |
Jakarta Persistence (JPA) | ✔︎ | ✔︎ | ✔︎ | ✔︎ | |
Jakarta RESTful Web Services (JAX-RS) | ✔︎ | ✔︎ | ✔︎ | ✔︎ | |
Jakarta Server Faces (JSF) | ✔︎ | ✔︎ | ✔︎ | ✔︎ | |
Jakarta Transactions (JTA) | ✔︎ | ✔︎ | ✔︎ | ✔︎ | |
Jakarta XML Binding (JAXB) | ✔︎ | ✔︎ | ✔︎ | ✔︎ | |
Jakarta Authentication (JAAS) | ✔︎ | ✔︎ | |||
Jakarta Authorization (JACC) | ✔︎ | ✔︎ | |||
Jakarta Concurrency | ✔︎ | ✔︎ | |||
Jakarta Connectors | ✔︎ | ✔︎ | |||
Jakarta Enterprise Web Services | ✔︎ | ✔︎ | |||
Jakarta Messaging (JMS) | ✔︎ | ✔︎ | |||
Jakarta SOAP with Attachments | ✔︎ | ✔︎ | |||
Jakarta Web Services Metadata | ✔︎ | ✔︎ | |||
Jakarta XML Web Services (JAX-WS) | ✔︎ | ✔︎ | |||
Jakarta Batch (JBatch) | ✔︎ |
현재 국내의 개발 환경이나 운영 환경에서는 이전에 잠시 EJB가 사용되었을 뿐 최근 전자정부 프레임워크에서도 보이듯 Enterprise 환경을 요구하는 상황은 거의 없는 것으로 보입니다. 아무래도 WAS 의존성을 낮추기 위해서는 벤더에서 주도하여 각각의 컴포넌트를 제공하는 Enterprise 구성 요소에 대한 의존도를 낮출 수 있는 방향이기는 하겠지요. 하지만 일부의 경우는 Enterprise 구성 요소를 잘 사용하기만 하면 어렵지 않게 안정적이고 보안적으로 보장되는 서비스의 구현이 가능할 것입니다.
부가적인 설명을 드리자면 JDK 버전과 Java Enterprise 버전은 서로 범위가 다릅니다. Java Standard기능은 JDK 버전과 같지만 Java Enterprise 기능은 JDK 버전과는 별개로 WAS에서 지원하는 컴포넌트의 요소에 따라 그 버전을 달리 합니다. 앞서 대표적인 국내 프레임워크로 Spring 프레임워크를 사용하는 전자정부 프레임워크를 예로 들었지만 국내와는 달리 국외 통계를 본다면 Java Enterprise의 사용은 그리 생소하지 않습니다.
따라서 Java Enterprise 환경을 고려한다면 톰캣을 기반으로 한 TomEE를 활용하는 방법도 톰캣에 익숙한 개발자들에게는 도움이 될 수 있겠습니다.
톰캣을 설치하는 OS 플랫폼 환경은 모든 환경을 지원합니다. 그나마 예전에는 일부 Unix/Linux/OSX 환경에서 Apache HTTP Server 설치하듯 컴파일을 통해 구성하였으나, 최근에는 압축파일을 해제하고 바로 사용할 수 있는 경우가 대부분입니다.
톰캣을 운영하기 위해 OS를 선택해야하는 입장이라면 다음과 같은 설치 타입을 고려할 수 있습니다.
톰켓의 버전이 올라감에 따라 지원하는 Java Standard Spec Version 또한 변경됩니다. 이 경우 일부 상위 버전은 JDK의 특정 버전에서 지원되지 않을 수 있지요. 따라서 개발되는 어플리케이션의 JDK요구치나 표준화된 톰캣 버전에 따라 지원되는 JDK 버전이 상이할 수 있습니다. 다음의 표를 참고하시기 바랍니다.
Servlet Spec | JSP Spec | EL Spec | WebSocket Spec | Authentication (JASPIC) Spec | Apache Tomcat Version | Latest Released Version | Supported Java Versions |
---|---|---|---|---|---|---|---|
6.0 | 3.1 | 5.0 | 2.1 | 3.0 | 10.1.x | 10.1.0-M8 (alpha) | 11 and later |
5.0 | 3.0 | 4.0 | 2.0 | 2.0 | 10.0.x | 10.0.14 | 8 and later |
4.0 | 2.3 | 3.0 | 1.1 | 1.1 | 9.0.x | 9.0.56 | 8 and later |
3.1 | 2.3 | 3.0 | 1.1 | 1.1 | 8.5.x | 8.5.73 | 7 and later |
3.1 | 2.3 | 3.0 | 1.1 | N/A | 8.0.x (superseded) | 8.0.53 (superseded) | 7 and later |
3.0 | 2.2 | 2.2 | 1.1 | N/A | 7.0.x (archived) | 7.0.109 (archived) | 6 and later (7 and later for WebSocket) |
2.5 | 2.1 | 2.1 | N/A | N/A | 6.0.x (archived) | 6.0.53 (archived) | 5 and later |
2.4 | 2.0 | N/A | N/A | N/A | 5.5.x (archived) | 5.5.36 (archived) | 1.4 and later |
2.3 | 1.2 | N/A | N/A | N/A | 4.1.x (archived) | 4.1.40 (archived) | 1.3 and later |
2.2 | 1.1 | N/A | N/A | N/A | 3.3.x (archived) | 3.3.2 (archived) | 1.1 and later |
톰캣 5.5.x 버전의 경우 5.5.12 버전 이후로는 JDK 5 이상을 지원함에 유의합니다.
Java SE의 경우 OS 플랫폼에 따라 제공하는 벤더가 다른 경우가 있습니다. Oracle이 서브스크립션 형태로, 업데이트에 대해 유료화 선언을 한 이후로 여러 파생 Java를 고려할 수 있습니다. 여전히 8 버전을 사용하는 서비스가 많아 OracleJDK가 점유율이 높으나, 이후 높은 버전으로 이전시에는 다른 JDK를 고려하는 상황도 발생할 것으로 보입니다.
Most Popular JRE/JDK Distribution (JRebel, 2020)
Provider | Free Builds from Source | Free Binary Distributions | Extended Updates | Commercial Support | Permissive License |
---|---|---|---|---|---|
AdoptOpenJDK | Yes | Yes | Yes | No | Yes |
Amazon – Corretto | Yes | Yes | Yes | No | Yes |
Azul Zulu | No | Yes | Yes | Yes | Yes |
BellSoft Liberica | No | Yes | Yes | Yes | Yes |
IBM | No | No | Yes | Yes | Yes |
OpenJDK Upstream | Yes | Yes | Yes | No | Yes |
Oracle JDK | No | Yes | No | Yes | No |
Oracle OpenJDK | Yes | Yes | No | No | Yes |
ojdkbuild | Yes | Yes | No | No | Yes |
RedHat | Yes | Yes | Yes | Yes | Yes |
SapMachine | Yes | Yes | Yes | Yes | Yes |
설치되는 톰캣의 최상위 경로는 $CATALINA_HOME
으로 표현합니다.
설치파일은 톰캣 홈페이지에서 좌측의 Downloads
에서 설치하고자 하는 버전을 선택하면 우측에 Binary와 Source Code 두가지 형태로 받을 수 있습니다. Binary는 OS플랫폼에 맞게 미리 준비된 설치파일로서 Core에 해당하는 파일을 받습니다. 종류는 여섯가지로 나뉘어 있습니다.
zip과 tar.gz는 Unix/Linux환경에서 사용할 압축된 형태의 파일이고 나머지 네가지는 CPU 아키텍쳐에 맞게 컴파일된 zip 형태의 압축 파일과 서비스에 등록할수 있는 형태의 인스톨러로 구성되어 있습니다. 설치하고자 하는 OS와 CPU 아키텍처에 맞는 설치파일을 받아 준비합니다.
Windows에는 압축파일을 풀어 설치하는 방법과 서비스 등록을 위한 인스톨러 두가지 방식이 있었습니다. 압축파일 형태의 경우 압축을 풀기만하면 바로 실행이 가능합니다. 서비스 인스톨러의 경우 서비스에 등록하기 위한 설치 경로와 같은 정보를 입력하여 진행합니다.
압축파일로 설치한 경우 %CATALINA_HOME%\bin
에 위치한 startup.bat
으로 시작하고 shutdown.bat
으로 종료합니다.
서비스로 설치한 경우 윈도우 서비스 관리 유틸리티에서 서비스의 '시작/종료'를 사용하거나 net start (서비스이름)
또는 net stop (서비스이름)
을 사용하여 서비스의 시작과 종료가 가능합니다.
Unix/Linux의 binary 설치파일은 압축을 풀어 설치합니다.
$CATALINA_HOME/bin
에 위치한 startup.sh
으로 시작하고 shutdown.sh
으로 종료합니다.
설치 방법은 매우 간단하나 설치 후 꼭 해야할 작업이 있습니다. Java Home을 설정하고 성능을 위한 Native library를 설치하는 작업 입니다.
Java Home의 경우 앞서 JDK를 OS에 설치하였다면 톰캣에서 이를 사용할 수 있도록 경로를 잡아주는 과정입니다. OS자체의 PATH나 환경변수로 지정하는 방법도 있고 톰캣의 스크립트에 변수로 넣어주는 방법이 있습니다. OS자체의 PATH로 설정하는 경우 해당 OS에 설치된 모든 Java 실행환경이 영향을 받게 됩니다. 따라서 일관된 서비스, 일관된 톰캣 운영 환경인 경우 같은 Java Home이 설정되는 장점이 있습니다. 이와달리 스크립트에 Java Home을 설정하면 해당 톰캣에서만 관련 설정의 영향을 받습니다. OS내에 서로 다른 JDK로 동작하는 서비스나 어플리케이션이 있다면 스크립트를 이용하는 방법을 사용할 수 있습니다.
OS 환경에 Java Home을 설정하는 방법은 다음과 같습니다. (Java Home은 JDK의 bin 디렉토리를 포함한 상위 디렉토리 입니다.)
계정 루트의 .profile
(bash 쉘의 경우 .bash_profile
)에 다음을 설정
# .bash_profile
+export JAVA_HOME=/usr/java/jre1.8.0_241
+
내컴퓨터 우클릭 > 고급 > 환경변수 > JAVA_HOME 추가
스크립트에 Java_Home을 설정하는 경우 catalina.sh(bat)
에 JAVA_HOME
으로 지정하는 경우가 있습니다. 추가로 setenv.sh(bat)
을 생성하여 해당 스크립트에 설정하는 방법을 권장합니다.
# setenv.sh
+JAVA_HOME="/usr/java/jre1.8.0_241"
+
# setenv.bat
+JAVA_HOME=C:\Progra~1\java\jre1.8.0_241
+
Windows환경에서 Java를 C:\Program Files
에 설치하는 경우 중간에 공백이 있기 때문에 C:\Progra~1
로 표현함에 주의합니다.
톰캣의 Native Library를 적용하지 않고도 충분히 톰캣을 실행하고 사용할 수 있습니다. 다만 톰캣의 콘솔 로그에 다음의 메시지가 걸리적(?)거리게 발생합니다.
The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path
+
굳이 필요하지 않다면 적용하지 않아도 되지만 Native Library가 주는 이점은 다음과 같습니다.
결국 NIO(Non-Blocking I/O)를 지원하는 것과 SSL관련 설정을 지원합니다. OpenSSL을 사용하지 않아도 NIO를 사용하는 성능상의 이점을 제공합니다.
Native Library를 사용하기 위해서는 APR과 Tomcat Native 소스 파일이 필요합니다. APR은 Apache 2.0 이상 버전이 설치되어있다면 해당 설치 경로의 $APACHE_HOME/bin/apr-1-config
과 같이 존재하나 없는 경우 별도의 APR을 설치합니다.
(Apache APR 홈페이지 : http://apr.apache.org/)
APR 홈페이지에서 받은 apr-x.x.x.tar.gz 파일은 다음의 순서를 따라 설치를 진행하며, 예제와 같이 /usr/local
에 설치하는 경우 root 계정이 필요합니다.
$ gzip -d apr-1.5.1.tar.gz
+$ tar -xvf apr-1.5.1.tar
+$ configure --prefix=/usr/local/apr-1.5.1
+$ make
+$ make install
+
일반적인 경우 Tomcat이 설치되면 설치된 경로의 "$CATALINA_HOME/bin" 위치에 tomcat-native.tar.gz 형태로 설치 파일이 존재하나 없는 경우, 혹은 높은 버전을 설치하고 싶은 경우 별도로 다운로드 받아 진행합니다. Native의 다운로드는 톰켓 홈페이지의 좌측 Downloads
의 하위에 Tomcat Native로 존재합니다. 관련 링크를 클릭하면 우측에 OS플랫폼에 맞게 소스를 받을 수 있는 링크가 제공됩니다.
Native Library는 다음과 같이 설치를 진행합니다.
$ gzip -d tomcat-native.tar.gz
+$ tar -xvf tomcat-native.tar
+$ configure --prefix=$CATALINA_HOME --with-apr=/usr/local/apr-1.5.1/bin/apr-1-config --with-java-home=$JAVA_HOME
+$ make
+$ install
+
소스의 컴파일과 설치가 완료되면 "$CATALINA_HOME/lib"에서 libtcnative 관련 파일의 확인을 할 수 있습니다.
Native Library가 해당 플랫폼 환경에 맞게 생성되었다면 Shared Library로 설정하여 적용하며 적용방법은 앞서 Java Home을 설정하는 방법을 이용합니다. Shared Library 환경변수명은 OS마다 상이함에 주의하며 setenv.sh(bat)에 적용하는 방법은 다음과 같습니다.
# setenv.sh(bat)
+LD_LIBRARY_PATH "$CATALINA_HOME/lib"
+
OS | Shared Library Path |
---|---|
Windows | PATH |
Solaris | LD_LIBRARY_PATH |
HP-UX | SHLIB_PATH |
AIX | LIBPATH,LD_LIBRARY_PATH |
Linux | LD_LIBRARY_PATH |
OS/2 | LIBPATH |
OSX | LD_LIBRARY_PATH |
적용된 Native Library는 톰캣의 콘솔 로그 "$CATALINA_HOME/logs/catalina.out(log)"에서 다음의 메시지를 확인 할 수 있습니다.
Info: Loaded APR based Apache Tomcat Native library 1.1.31.
+
리스너는 Tcp로 활성화되는 HTTP 프로토콜을 상징하여 설명합니다. 톰캣은 기본적으로 HTTP, AJP, Shutdown 을 위한 port가 활성화 됩니다. 리스너는 우리 몸의 귀와 같은 역할을 합니다. 들려오는 요청을 받는 역할을 하지요. 톰캣 또한 요청을 받아들이기 위해 리스너를 활성화하여 요청을 받아 들입니다.
이러한 리스너는 톰캣의 "Coyote" 컴포넌트가 담당하는데 톰캣의 Startup.sh(bat)
을 수행하면 다음과 같은 메시지를 확인할 수 있습니다.
7월 15, 2014 5:46:18 오후 org.apache.coyote.AbstractProtocol start 정보: Starting ProtocolHandler ["http-bio-8080"]
+7월 15, 2014 5:46:18 오후 org.apache.coyote.AbstractProtocol start 정보: Starting ProtocolHandler ["ajp-bio-8009"]
+7월 15, 2014 5:46:18 오후 org.apache.catalina.startup.Catalina start 정보: Server startup in 1002 ms
+
http에 8080포트가 할당되고 ajp에 8009포트가 할당됩니다. 이런 포트로 톰캣에 HTTP 혹은 AJP로 요청을 전달할 수 있습니다. 만약 톰캣이 기동된 서버의 IP가 "192.168.0.10"이고 사용되는 HTTP 포트가 8080 이라면
http://192.168.0.10:8080
이렇게 브라우저에서 호출이 가능합니다. 해당 IP가 DNS나 별도의 호스팅 서비스를 통해 www.tomcat-gm.com
에 연결되어 있다면
http://www.tomcat-gm.com:8080
이렇게 호출이 가능합니다.
일반적으로는 HTTP의 기본 포트로 80이 사용되고 HTTPS(SSL)의 기본 포트로 443이 사용됩니다. 이경우 별도의 포트 지정없이 url 요청이 가능합니다.
http://www.tomcat-gm.com(:80)
https://www.tomcat-gm.com(:443)
이러한 리스너 설정은 톰캣의 설정에서 Connector
로 정의됩니다. $CATALINA_HOME/conf
디렉토리의 server.xml
을 열어보시면 다음과 같은 Connector 설정을 확인 할 수 있습니다.
<Connector port="8080" protocol="HTTP/1.1"
+ connectionTimeout="20000"
+ minSpareThreads="10"
+ maxSpareThreads="5"
+ maxThreads="15"
+ redirectPort="8443" />
+
프로토콜의 형태가 "HTTP/1.1"이고 포트는 "8080"으로 활성화 됩니다. 해당 플랫폼에 IP가 여러개 존재한다면 "address" 항목을 추가하여 별도의 IP를 지정 할 수도 있습니다. 이같은 설정은 AJP나 SSL 또한 마찬가지 입니다.
Java의 장점이 무엇인가 물으면 그 대표적인 한가지는 OS 플랫폼에 종속적이지 않은 어플리케이션 개발이 가능하다 일 것입니다. 이런 개발 환경이 가능한 이유는 JVM(Java Virtual Machine)이 제공하는 환경 때문입니다. JVM이 동작하면 각 OS에 Java가 공통적으로 수행되기 위한 Runtime 환경을 만들고 이후 생성된 JVM 환경에서 어플리케이션이 수행되기 때문에 OS 플랫폼 마다 개발을 달리하지 않아도 됩니다. 하지만 각각의 플랫폼에서의 JVM은 그 동작의 목적은 같아도 어플리케이션에 따라 성능에 차이가 발생할 수 있습니다. 어떤 어플리케이션은 한번에 큰 메모리를 요구하는가 하면 때로는 계산을 주로 한다던가, IO 작업이 많다던가하여 CPU 자원을 많이 필요로 하는 식이죠. 따라서 JVM에서는 사용자가 조절할 수 있는 수많은 옵션을 제공합니다. 물론 아무것도 설정하지 않은 상태가 가장 일반적일 수는 있지만 성능이나 장애극복을 위해 Java Option이 추가되기도 합니다. 적용되는 Java Option의 예는 다음과 같습니다.
옵션 | 설명 |
---|---|
-Xms(ms) | Heap 메모리의 최소값을 정의합니다. |
-Xmx(mx) | Heap 메모리의 최대값을 정의합니다. |
-verbosegc | JVM에서 수행하는 GC를 로그로 남깁니다. |
-XX:+AggressiveOpts | 소수점 컴파일을 최적화 합니다. |
-Djava.net.preferIPv4Stack=true | IPv4와 IPv6모두 사용할 수 있는 환경에서 IPv4를 우선하여 서비스 합니다. |
이 외에도 수많은 Java Option이 존재하기에 각 환경에 맞는 Java Option의 적용이 필요하겠습니다. 하지만 어플리케이션이 실제 수행되기 전에는 어떤 요구사항이 발생하는지는 알 수 없기 때문에 반드시 실 서비스를 하기 전 충분한 테스트가 필요합니다.
톰캣에서 Java Options의 추가를 위해서는 setenv.sh(bat)
혹은 catalina.sh(bat)
의 스크립트에 추가하는 방법과 Windows 서비스에 등록된 경우 관련 설정창에 추가하는 방법이 있습니다.
(서비스로 등록된 톰캣에 Java Options 적용 예)
Java 환경에서는 class를 호출하여 서비스를 수행합니다. 각 class는 단독으로, 혹은 여러개가 함께 각각의 Class에 정의된 역할을 수행합니다. 이런 Class를 사용하기 위해서는 ClassLoader가 Class를 읽어 Class를 나열하는 과정이 수행됩니다. 나열되는 Class들은 경로의 형태를 띄며 이를 ClassPath라고도 부릅니다.
...:class:class:class:class:...
ClassLoader가 Class를 읽지 못한다면 JVM에서는 해당 Class에 들어있는 Method를 요청할 때 찾지 못하는 상황이 발생하며, 이경우 ClassNotFound
메시지를 발생시킵니다.
또한 이렇게 정의되는 ClassPath에는 우선순위가 있습니다. 동일한 Class명의 동일한 Method이지만 다른 역할을 수행하는 Class가 로딩된다면 어떤것이 우선권을 갖을까요? ClassPath순서상 앞서있는 Class가 우선권을 갖습니다. 아래와 같은 ClassPath가 생성되었다면 classA가 우선권을 갖습니다.
...:classA:classB:classC:classE:...
그렇다면 어플리케이션 개발자가 의도한 Class를 호출하기 위해서는 ClassLoader가 원하는 class를 앞서 설정하도록 해야합니다. 물론 겹치는 Class가 없다는 가정하에는 어떤 위치에 있던지 상관없이 읽히기만 하면 되겠지요.
일반적으로 웹 어플리케이션을 위한 war
형태의 애플리케이션 개발시 Class는 WEB-INF/classes
jar형태의 라이브러리는 WEB-INF/lib
에 위치시킵니다. 이렇게 위치된 Class들은 톰캣 혹은 WAS에 배치(deploy)되면 전체 JVM의 가장 뒤에 위치하게 됩니다. 간혹 웹 어플리케이션 형태가 아닌 Class나 라이브러리를 적용하기위해서는 CLASSPATH
라는 변수를 사용하여 ClassLoader가 읽을 수 있도록 합니다. 해당 변수는 WAS마다 상이할 수 있습니다.
실행되는 JVM에서의 ClassLoader 순서를 보면 다음과 같습니다.
BootClassPath:ExtensionClassPath:ClassPath
BootClassPath와 ExtensionClassPath는 Java의 기본 라이브러리를 로딩합니다. rt.jar와 같은 필수 라이브러리가 그 예입니다. 만약 기존 JVM을 hooking하는 식의 클래스를 사용하는 경우에는 이보다 앞서 클래스를 위치시킬 필요가 있습니다. HelloWorld라는 클래서를 BootClassPath앞에 위치하게 하려면 -Xbootclasspath/p:HelloWorld
, 뒤에 적용하려면 -Xbotclasspath/a:HelloWorld
형태를 사용하여 적용합니다. p와 a에 주의합니다. 그리고 일반적인 Class가 위치하는 ClassPath에 위치하게 하기 위해서는 톰켓의 경우 스크립트에 'CLASSPATH'변수를 치환합니다. 그 예는 다음과 같습니다.
export CLASSPATH=HelloWorld
+export CLASSPATH=${CLASSPATH}:HelloWorld
+export CLASSPATH=HelloWorld:${CLASSPATH}
+
기 적용된 CLASSPATH가 있는가에 따라 적용하고자하는 Class 혹은 라이브러리 앞, 뒤에 기존 CLASSPATH를 넣어줄 수도 있습니다.
경고
Windows의 경우 구분자가 세미콜론(;)이고 그 외에는 콜론(:)임에 주의합니다.
Windows 환경의 서비스 실행방법을 제외하고는 대부분 스크립트에 앞서 설명한 Java Option이나 ClassPath를 설정합니다. 일반적으로, 그리고 여러 운영환경에서 이러한 실행 환경 변수를 catalina.sh(bat)
에 설정하여 사용하는 경우를 보았습니다. 하지만 한번이라도 해당 스크립트를 열어 읽어보셨다면 다음과 같은 메시지를 확인 할 수 있을 것입니다.
# -----------------------------------------------------------------------------
+# Control Script for the CATALINA Server
+#
+# Environment Variable Prerequisites
+#
+# Do not set the variables in this script. Instead put them into a script
+# setenv.sh in CATALINA_BASE/bin to keep your customizations separate.
+
즉, catalina.sh
는 건드리지 말고 setenv.sh(bat)
에 추가적은 설정을 하라는 안내 문구 입니다. catalina.sh
를 수정하는 경우 해당 톰캣을 이전하거나, 동일한 톰캣을 구성하거나, 또는 복구해야 하는 상황에서 추가로 설정되거나 수정된 내용의 확인이 힘들 수 있고, 또한 설정과 수정으로 인한 비정상 동작을 할 수 있기 때문입니다. 그렇다면 setenv.sh(bat)
은 어떻게 작용할까요? catalina.sh(bat)
에서 setenv
를 찾아보면 다음과 같이 setenv
스크립트에 적용된내용을 읽어오는 것을 확인 할 수 있습니다.
if [ -r "$CATALINA_BASE/bin/setenv.sh" ]; then
+ . "$CATALINA_BASE/bin/setenv.sh"
+elif [ -r "$CATALINA_HOME/bin/setenv.sh" ]; then
+ . "$CATALINA_HOME/bin/setenv.sh"
+fi
+
rem Get standard environment variables
+if not exist "%CATALINA_BASE%\bin\setenv.bat" goto checkSetenvHome
+call "%CATALINA_BASE%\bin\setenv.bat"
+goto setenvDone
+:checkSetenvHome
+if exist "%CATALINA_HOME%\bin\setenv.bat" call "%CATALINA_HOME%\bin\setenv.bat"
+:setenvDone
+
이같이 톰캣에서는 추가/수정해야하는 환경변수나 설정값을 하나의 스크립트에서 관리할 수 있는 환경을 제공합니다. 다만 setenv.sh(bat)
스크립트는 별도로 만들어야 합니다. 앞서 catalina.sh(bat)
의 설명된 변수들을 보면 Java Options은 JAVA_OPTS
로하지 말고 CATALINA_OPTS
로 하라는 점도 주의해서 보시기 바랍니다. JAVA_OPTS
의 경우 톰캣을 정지시키는 shutdown.sh(bat)
에서도 호출되기 때문에 차후 소개되는 JMX 모니터링을 위한 옵션과 같이 별도의 포트를 활성화하는 옵션과 같은 성격의 설정 적용시 문제가 될 수 있습니다. setenv.sh(bat)
스크립트에 다음과 같이 추가하면 해당 옵션을 별도로 export하지 않아도 톰캣 기동시 적용됩니다.
CATALINA_OPTS="-Xms1024m -Xmx2048m -XX:MaxPermSize=512m -verbosegc"
+CLASSPATH="${CLASSPATH}:/app/libs/myapi.jar"
+
set CATALINA_OPTS=-Xms1024m -Xmx2048m -XX:MaxPermSize=512m -verbosegc
+set CLASSPATH=%CLASSPATH%;/app/libs/myapi.jar
+
웹 어플리케이션에서 web.xml
은 서블릿을 정의하고 이어주는 역할을 수행합니다. 이와 마찬가지로 톰캣에 있는 $CATALINA_HOME/conf/web.xml
또한 톰캣에 있는 서블릿을 정의하고 이어주는 역할을 수행합니다. 다만 JSP/Servlet 엔진으로서의 최소한의 정의를 합니다.
따라서 톰캣에 배치되는 모든 어플리케이션에서 공통으로 수정되어야 할 사항은 web.xml에도 정의할 수 있습니다. 하지만 앞서 catalina
스크립트와 마찬가지로 추가/수정시 부작용이 있을 수 있음에 중의합니다.
톰캣의 로그는 다음과 같이 종류와 정의는 다음에서 정의합니다. 다만 Windows 서비스는 서비스 환경설정에서 정의힙니다.
Log | Config File |
---|---|
Catalina.out | CATALINA_OUT 환경변수로 정의, catalina.sh에 정의되어 있고 setenv 스크립트에서 재정의 |
access.log | server.xml |
*.log | logging.properties |
톰캣에 배치되는 어플리케이션은 Java Web Application입니다. 간단히 웹 어플리케이션이라고도 합니다. 간략한 구조는 다음과 같습니다.
파일 구조
./APPDIR
+├── WEB-INF
+│ ├── classes
+│ │ └── class-files
+│ ├── lib
+│ │ └── jar-files
+│ └── web.xml
+├── index.html
+└── index.jsp
+
APP 디렉토리 하위에는 웹어플리케이션의 정의를 넣을 WEB-INF 디렉토리가 필요합니다. 아주 간단한 어플리케이션은 web.xml
에 다음의 태그만 넣어도 웹 어플리케이션으로 인지할 수 있습니다.
<web-app/>
+
어플리케이션을 배치하는 방법에는 톰캣에서 제공하는 manager
를 사용하는 방법이 있습니다. manager
는 톰캣을 설치하면 제공되는 어플리케이션 관리 툴로 다음과 같이 확인할 수 있습니다.
$CATALINA_HOME/conf/tomcat-user.xml
에 설정하는 방법을 에러페이지에서 확인tomcat-user.xml
에 다음과 같이 설정 추가 (e.g. user/passwd
를 admin/admin
으로 설정)<tomcat-users>
+ <role rolename="manager-gui"/>
+ <user username="admin" password="admin" roles="manager-gui"/>
+</tomcat-users>
+
manager의 중간에 Deploy
에서 배치를 수행할 수 있으며 두가지 타입이 제공됩니다. 한가지는 Deploy directory or WAR file located on server
로서 톰캣이 기동된 서버내의 어플리케이션을 지정하여 배치하는 방법과 WAR file to deploy
는 현재 접속중인 로컬의 WAR파일을 업로드하여 배치하는 방법입니다. 두 방법 모두 수행 후 $CATALINA_HOME/webapps
에 해당 어플리케이션이 위치하게 됩니다.
톰캣에는 자동으로 어플리케이션을 인지하고 배치하는 디렉토리가 있습니다. 바로 $CATALINA_HOME/webapps
입니다. 해당 경로는 앞서 manager 를 통한 배치시에도 어플리케이션이 위치하게되는 경로인데, manager를 사용하는 방법은 결국 webapps 디렉토리에 어플리케이션을 위치시키는 작업이라고 볼 수 있습니다. 따라서 직접 해당 경로에 어플리케이션을 위치시켜도 동일하게 배치 작업이 발생합니다.
webapps 디렉토리가 자동으로 배치를 수행하는 설정은 server.xml
에서 해당 경로가 배치를 수행하도록 설정되었기 때문입니다.
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
+ <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
+ prefix="localhost_access_log." suffix=".txt"
+ pattern="%h %l %u %t "%r" %s %b" />
+</Host>
+
server.xml
에서 확인 할 수 있는 톰캣의 기본 host 환경인 localhost
에서 webapps 디렉토리를 대상으로 autoDeploy를 수행한다는 설정 내용입니다. 이러한 설정으로 인해 자동으로 어플리케이션의 배치가 가능합니다.
앞서 두가지의 manager나 webapps 디렉토리를 통한 배치 방법은 모두 톰캣 내부의 webapps 디렉토리에 어플리케이션이 위치하게 된다는 특징이 있습니다. 이러한 점은 사용자가 원하는 임의의 위치에 어플리케이션은 배제 된다는 의미 일 수도 있습니다. 따라서 이경우 context.xml
형태의 xml을 통한 배치 방법을 사용 할 수 있겠습니다.
배치를 설명하기에 앞서 context.xml
의 context
디스크립터는 본래 server.xml에 위치하는 디스크립터였습니다. 하지만 해당 디스크립터에 설정되는 내용들은 변경사항이 자주 발생하는 항목들이기에 별도의 xml에서도 정의할 수 있도록 변경되었으며, 톰캣 5.5.12 버전부터는 server.xml이 아닌 별도의 context.xml을 통하여 해당 설정들을 관리하도록 권고하고 있습니다.
context.xml
에서 설정하는 정보는 어플리케이션 뿐만이 아니기 때문에 어플리케이션 설정을 제외한 설정을 톰캣에 적용하는 경우에도 사용됩니다. context.xml
은 <Context>
디스크립터로 시작되며 다음의 위치에서 적용되고, 위치에 따라 적용 범위가 달라집니다.
이같은 위치에 따른 적용 범위는 context로 정의되는 대표적인 자원중 하나인 데이터소스(DataSource)의 경우 톰캣전체 또는 어플리케이션 별로 구분할 수 있는 기능을 사용할 수 있습니다.
어플리케이션을 배포하는 경우 위의 4가지 방법 중 3, 4번 항목을 들 수 있으며, 특히 context.xml
을 사용한 임의의 위치의 어플리케이션 배포는 3번 항목을 사용하게 됩니다.
sample
context-root를 갖는 어플리케이션은 다음과 같이 context.xml
을 설정할 수 있습니다.
<?xml version="1.0" encoding="UTF-8"?>
+
+<Context path="sample" docBase="/Users/GSLee/APP/sample" debug="0" reloadable="true" crossContext="true" privileged="true"/>
+
+<!-- path는 해당 설정을 server.xml에 하는 경우 의미가 있고 3번 방법의 경우 xml 파일 이름이 context-root로 설정됩니다. -->
+
일반적으로 어플리케이션을 배치하는 경우 해당 어플리케이션의 디렉토리 이름이나 context로 설정된 xml의 파일 이름이 context-root로 사용됩니다.
http://www.mytomcat.co.kr/[WEBAPPNAME]/index.jsp
하지만 대부분의 경우 다음과 같이 요청되기를 바라죠.
http://www.mytomcat.co.kr/index.jsp
이경우 배치 방식은 동일하지만 다음의 네가지 방법을 통해 어플리케이션 배치 시 context-root를 /
로 설정할 수 있습니다.
구성 위치 | 설명 |
---|---|
$CATALINA_HOME/webapps/ROOT | webapps 기본 디렉토리에 ROOT인 디렉토리명으로 배치된 어플리케이션 |
$CATALINA_HOME/conf/[ENGINENAME]/[HOSTNAME]/ROOT.xml | ROOT를 이름으로 갖는 context 타입의 xml로 배치된 어플리케이션 |
Tomcat Manager | APP에서 context path 항목을 비워놓은 채로 배치하는 어플리케이션 |
server.xml에 배치 어플리케이션을 설정 | context 디스크립터의 path 항목을 비워놓음 |
방법은 여러가지가 있지만 앞서 설명드린 context
디스크립터로 설정한 별도의 xml을 사용한 배치 방식을 권장합니다.
sample 어플리케이션을 ROOT로 배치한 로그는 다음과 같이 확인됩니다.
정보: Starting Servlet Engine: Apache Tomcat/8.5.73
+9월 06, 2014 8:30:52 오후 org.apache.catalina.startup.HostConfig deployDescriptor
+정보: Deploying configuration descriptor /Users/GSLee/APP/tomcat/apache-tomcat-8.5.73/conf/Catalina/localhost/ROOT.xml
+9월 06, 2014 8:30:52 오후 org.apache.catalina.core.StandardContext setPath
+경고: A context path must either be an empty string or start with a '/'. The path [sample] does not meet these criteria and has been changed to [/sample]
+9월 06, 2014 8:30:52 오후 org.apache.catalina.startup.SetContextPropertiesRule begin
+경고: [SetContextPropertiesRule]{Context} Setting property 'debug' to '0' did not find a matching property.
+9월 06, 2014 8:30:53 오후 org.rhq.helpers.rtfilter.filter.RtFilter init
+정보: Initialized response-time filter for webapp with context root 'ROOT'.
+9월 06, 2014 8:30:53 오후 org.apache.catalina.startup.HostConfig deployDescriptor
+정보: Deployment of configuration descriptor /Users/GSLee/APP/tomcat/apache-tomcat-8.5.73/conf/Catalina/localhost/ROOT.xml has finished in 1,115 ms
+
Auto Deploy와 Hot Deploy는 Auto와 Hot을 어떻게 해석하는가에 따라 다음과 같이 혼용되어 사용됩니다.
의미가 어떻게 해석되던지 이런 용어를 사용함에 있어서 바라는점은 서비스가 실행중인 도중에도 변경사항을 사용자 모르게 바꾸고자 하는 의도가 대부분일 것입니다.
webapps 디렉토리에 어플리케이션을 넣으면 자동으로 배치가 됩니다. 서버가 기동된 상태에서도 말이죠. 해당 설정은 다음의 'Host' 디스크립터에서 정의합니다.
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
+ ...
+</Host>
+
Host 설정에 autoDeploy가 true
인 경우 해당 디렉토리에 위치하는 어플리케이션을 감지하여 자동으로 톰캣에 배치를 수행합니다.
jsp를 사용자 뷰로 사용하는 경우 서비스의 컨텐츠, 또는 jsp에서 실행하는 코드상의 변경사항이 자주 발생하게 됩니다. 이경우 jsp를 새로 반영하기 위해 서버가 실행중임에도 자동으로 업데이트된 jsp를 컴파일하여 해당 소스를 반영하는 동작을 지원하는 설정이 있습니다. 해당 설정은 다음의 $CATAILNA_HOME/conf/web.xml
의 jsp 서블릿에 정의되어 있습니다.
<servlet>
+ <servlet-name>jsp</servlet-name>
+ <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
+ ...
+ <init-param>
+ <param-name>development</param-name>
+ <param-value>true</param-value>
+ </init-param>
+ <init-param>
+ <param-name>checkInterval</param-name>
+ <param-value>1</param-value>
+ </init-param>
+ <load-on-startup>3</load-on-startup>
+</servlet>
+
jsp 서블릿에서는 두가지 경우에 대해 jsp의 업데이트를 수행합니다.
development
가 true
인 경우 항상 확인development
가 false
이고 checkInterval
이 0
보다 큰 경우 확인합니다.관련 옵션에 대한 상세 내용이나 추가적인 jasper 컴포넌트의 옵션은 해당 설정의 위에 주석으로 설명이 되어있습니다.
클래스를 수정하여 컴파일한 뒤 어플리케이션에 업로드하면 얼마 후 해당 어플리케이션(컨텍스트)를 리로딩하는 과정을 수행합니다. 해당 설정은 Context
디스크립터가 설정된 xml에 정의합니다.
<Context reloadable="true" ...>
+ <Loader checkInterval="15"/>
+</Context>
+
reloadable
을 true
로 설정하는 경우 해당 어플리케이션(컨텍스트)는 클래스에 변경이 발생하면 다시 리로딩하는 기능을 수행합니다. 간격은 기본 15초이기 때문에 더 빠른 방영을 원하시면 Context
디스크립터 내에 Loader
의 checkInterval
을 정의함으로 시간을 조절할 수 있습니다. 하지만 이런 리로드 현상을 싫어하시는 분도 있습니다. "Tomcat Context 수동 Reload"라는 블로그 글에서도 보이듯 자동 리로드를 비활성화 하고 Valve
를 사용하는 별도의 방법을 사용할 수도 있습니다.
어플리케이션의 클래스가 수정되어 리로드가 발생하거나 라이브러리나 xml등의 재기동 후 반영되는 변경사항을 적용하기 위해서는 톰캣을 재기동해야 하는 상황이 발생합니다. 이런 경우 기존 어플리케이션은 기존에 사용중인 사용자가 계속 이용 할 수 있도록 활성화된 상태에서 새로운 버전의 어플리케이션을 배치, 새로운 사용자는 새로운 어플리케이션을~~(새술은 새부대에?)~~ 사용하도록 하는 Parallel Deployment 기능을 사용 할 수 있습니다.
WebLogic Server 같은 타 WAS에서도 이와 유사한 'production redeployment'기능이 있지만 톰캣이 좀더 쉬운 방법을 제공합니다.
그 방법은 WEBAPP##[VersionNumber]
입니다. sample 어플리케이션으로 예를 들면 sample##1.0
으로 배치를 수행하는 것입니다. 기존 어플리케이션 뒤에 샵 기호 두개와 버전이름만 붙이면 되기 때문에 매우 간단하지만 단점으로는 거의 동일한 구성과 용량의 독립적인 어플리케이션이 필요하기 때문에 어느정도 변경사항이 많은 경우 활용도가 높겠습니다. 버전은 float
형태로 정의하며 상대적으로 높은 값이 신규 배치가 됩니다. Context에서 지정하는 경우에는 어플리케이션 경로를 설정한 대로 샵 기호가 추가된 어플리케이션 이름을 지정하면 서비스 컨텍스트는 샵 기호와 버전이 제외된 기존 경로를 사용하게 됩니다.
<Context docBase="/app/sample##01" ... /></Context>
+
<Context docBase="/app/sample##02" ... /></Context>
+
배치된 어플리케이션은 톰캣 Manager에서도 확인할 수 있습니다.
버전이 높은 어플리케이션이 배치되면 기존 사용자는 이전 버전의 어플리케이션 서비스를 이용하고 새로 접속하는 사용자는 신규 어플리케이션의 서비스를 이용하게 됩니다. 이전 버전의 어플리케이션은 톰캣 Manager에서 Session을 확인하여 사용자가 없는것을 확인 후 제거할 수 있습니다.
JDBC Connection Pool은 Java에서 DB(Data Base)의 Session 자원을 미리 확보함으로 재생성하는 비용을 줄이기 위한 기술 입니다. Java에서 사용되는 기술이기 때문에 각 DB 벤더사들은 Java로 구현되는 서비스에서 자사의 DB를 사용할 수 있도록 별도의 라이브러리를 제공하며 이를 사용하여 DB와의 Connection을 생성하고 DB를 사용할 수 있게 됩니다.
JDBC는 여타 드라이버와는 다르게 미리 Connection을 확보하여 JVM상에 Object상태로 만들어두고 이를 요청하는 서비스에 빌려줍니다. 빌려준다는 표현을 사용한 이유는 반드시 반환되어야 하기 때문입니다. 앞서 미리 만든다는 표현은 만드는 개수가 제한되어 있다는 의미로 사용하였으며, 때문에 한정된 자원을 DB와의 연계 처리만을 하는 경우 잠시 사용하고 다시 반납하는 과정을 거칩니다.
다음은 jsp에서 단일 Oracle DB와의 Connection Pool을 생성하고 반납하는 샘플 코드입니다.(테스트 용도로 입니다.)
<%@ page import="java.sql.*" %>
+<%
+ StringBuffer sbError = new StringBuffer();
+ DatabaseMetaData dbMetaData = null;
+ Connection conn = null;
+%>
+<font size="-1"><p>
+<%
+ DriverManager.registerDriver (new oracle.jdbc.OracleDriver());
+ try {
+ conn = DriverManager.getConnection("jdbc:oracle:thin:@172.16.1.128:1521:TOSA1", "fmsvr", "fmsvr");
+ dbMetaData = conn.getMetaData();
+%>
+<p>
+Name of JDBC Driver: <%= dbMetaData.getDriverName() %><br>
+Version: <%= dbMetaData.getDriverVersion() %><br>
+Major: <%= dbMetaData.getDriverMajorVersion() %><br>
+Minor: <%= dbMetaData.getDriverMinorVersion() %><br>
+<p>
+Database Name: <%= dbMetaData.getDatabaseProductName() %><br>a
+Version: <%= dbMetaData.getDatabaseProductVersion() %><br>
+<%
+ } catch (SQLException e) {
+ sbError.append(e.toString());
+ } finally {
+ if (conn != null) {
+ try {
+ conn.close();
+ } catch (SQLException e) {
+ sbError.append(e.toString());
+ }
+ }
+ }
+ if (sbError.length() != 0) {
+ out.println(sbError.toString());
+ } else {
+%>
+<p>No error</font>
+<%
+ }
+%>
+
주어진 정보로 getConnection()을 수행하고 다시 close()를 수행하여 반납하는 과정이며, close()하지 않는 경우 해당 객체는 모두 사용하였음에도 불구하고 메모리상에 남아 차후 메모리 이슈를 발생시킬 수 있습니다.
톰캣에서는 이런 일련의 Connection을 생성하는 작업을 어플리케이션 대신 생성할 수 있으며 내부적으로 생성하는 개수나 연결이 끊어졌을 때의 재시도, 사용하지 않는 Connection의 강제 반환 등의 설정이 가능합니다.
다음은 Context
디스트립터 내에 설정하는 Resource
에서 정의한 DataSource 예제 입니다.
<Resource name="jdbc/test"
+ auth="Container"
+ type="javax.sql.DataSource"
+ username="oracle"
+ password="oracle"
+ driverClassName="oracle.jdbc.driver.OracleDriver"
+ url="jdbc:oracle:thin:@address:1521:SID"
+ removeAbandoned="true"
+ removeAbandonedTimeout="60"
+ logAbandoned="true"
+ maxActive="25"
+ maxIdle="10"
+ maxWait="-1"
+/>
+
톰캣에서 DB를 연동하기 위해서는 우선 사용할 DB의 벤더사에서 제공하는 JDBC driver를 ClassLoader에서 읽도록 해야 합니다. 우선 JDBC driver를 받고 두가지 방법으로 적용이 가능합니다.
#JDBC Driver Classpath
+CLASSPATH=/app/lib/jdbc.jar
+
대표적인 DB의 Context
디스크립터에 설정하는 방법은 다음과 같습니다.
<!-- MySQL - Connector/J -->
+<Resource name="jdbc/test"
+ auth="Container"
+ type="javax.sql.DataSource"
+ username="javauser"
+ password="javadude"
+ driverClassName="com.mysql.jdbc.Driver"
+ url="jdbc:mysql://ipaddress:3306/javatest"
+ maxActive="25"
+ maxIdle="10"
+ maxWait="-1"
+/>
+
<!-- Oracle - classes12.jar(jdk1.4.2), ojdbc#.jar(5+) -->
+<Resource name="jdbc/test"
+ auth="Container"
+ type="javax.sql.DataSource"
+ username="oracle"
+ password="oracle"
+ driverClassName="oracle.jdbc.driver.OracleDriver"
+ url="jdbc:oracle:thin:@ipaddress:1521:SID"
+ maxActive="25"
+ maxIdle="10"
+ maxWait="-1"
+ />
+
<!-- PostgreSQL - JDBC # -->
+<Resource name="jdbc/test"
+ auth="Container"
+ type="javax.sql.DataSource"
+ username="myuser"
+ password="mypasswd"
+ driverClassName="org.postgresql.Driver"
+ url="jdbc:postgresql://ipaddress:5432/mydb"
+ maxActive="25"
+ maxIdle="10"
+ maxWait="-1"
+/>
+
Resource
에 정의되는 항목은 기본적인 url이나 DB접근 계정정도만 있어도 가능하지만 간혹 튜닝이나 문제해결을 위해 추가적인 옵션이 요구되는 경우가 있습니다. 톰캣에서는 다음의 설정값을 제공합니다.
ATTRIBUTE | DESCRIPTOIN | DEFAULT |
---|---|---|
maxActive | 최대 Connection 값 | 100 |
maxIdle | Idle Connection 최대 허용치 | =maxActive |
minIdle | Idle Connection 최소 허용치 | =initialSize |
initialSize | Connection Pool의 최초 생성 개수 | 10 |
maxWait | Connection을 얻기위해 대기하는 최대 시간 | 30000(ms) |
removeAbandoned | 특정시간 동안 사용하지 않는 Connection 반환 | false |
removeAbandonedTimeout | removeAbandoned가 동작하는데 소요되는 시간 | 60(s) |
logAbandoned | Connection이 remove될 때 log에 기록 | false |
testOnBorrow | getConnection()이 수행될 때 유효성 테스트 | false |
validationQuery | 테스트를 위한 쿼리 | null |
validationQuery
로는 다음과 같이 적용 가능합니다.
http://tomcat.apache.org/tomcat-10.0-doc/jdbc-pool.html
톰캣에서 생성한 JDBC Connection Pool을 DataSource로서 사용하기 위해서는 JNDI를 Lookup하는 방법을 사용합니다. JNDI를 사용하면 이를 지원하는 다른 프레임워크나 API에서도 톰캣의 자원을 사용할 수 있습니다. mybatis/ibatis의 경우도 JNDI 설정을 할 수 있습니다.
Context
디스크립터로 정의한 DataSource는 어플리케이션의 web.xml
에서 정의하고 소스에서는 lookup
을 이용하여 사용합니다. 일련의 설정방법의 예는 다음과 같습니다. Context의 정의의 위치에 따라 전체 어플리케이션에 적용될 수도 있고 host단위, 혹은 단일 어플리케이션 내에서만 자원을 생성하게 됩니다.
<!-- context.xml -->
+<Resource name="jdbc/test"
+ auth="Container"
+ type="javax.sql.DataSource"
+ username="oracle"
+ password="oracle"
+ driverClassName="oracle.jdbc.driver.OracleDriver"
+ url="jdbc:oracle:thin:@ipaddress:1521:SID"
+ />
+
<!-- web.xml -->
+<web-app>
+ ...
+ <resource-ref>
+ <res-ref-name>jdbc/test</res-ref-name>
+ <res-type>javax.sql.DataSource</res-type>
+ <res-auth>Container</res-auth>
+ </resource-ref>
+ ...
+</web-app>
+
//Source Code
+ds = ctx.lookup("java:comp/env/jdbc/test");
+
톰캣에 정의된 바로는 Host
로 정의되나 일반적인 기능으로 표현한다면 가상 호스트(Virtual Host)와 같은 기능 입니다. 특정 host 명, 즉 http url로 서비스를 분기하는 역할을 합니다. server.xml
기본으로 설정되어있는 localhost
인 호스트의 내용은 다음과 같습니다.
<Engine name="Catalina" defaultHost="localhost">
+ <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
+ ...
+ </Host>
+</Engine>
+
설정된 내용을 분석해본다면 Catalina
라는 톰캣의 엔진에서 처리하는 기본 호스트는 localhost
이고, localhost
는 webapps
디렉토리를 기본 배치 디렉토리고 갖는다는 내용입니다. 기본 호스트로 지정된 호스트는 이외에 설정된 호스트 조건에 맞지 않은 모든 요청을 처리하게 됩니다. 이렇게 생성된 localhost
는 $CATALINA_HOME/conf/[ENGINENAME]/[HOSTNAME]
과 같은 경로에 호스트만의 설정 값을 갖게 됩니다.
별도의 호스트 추가는 'Engine' 디스크립터 하위에 'Host' 디스크립터로 정의합니다. 'myhost'라는 호스트는 다음과 같이 추가할 수 있습니다.
<Engine name="Catalina" defaultHost="localhost">
+ <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
+ ...
+ </Host>
+ <Host name="myhost" appBase="webapps_myhost" unpackWARs="true" autoDeploy="true">
+ ...
+ </Host>
+</Engine>
+
새롭게 추가되는 호스트는 기본 배치경로를 다르게 설정합니다. 때문에 동일한 컨텍스트의 요청이라도 어떤 호스트가 처리하는가에 따라 다른 어플리케이션의 서비스를 이용하게 됩니다. 설정 후 톰캣 프로세스를 재기동하면 호스트 설정 디렉토리가 생성됨을 확인 할 수 있습니다.
호스트의 기능은 주로 웹서버에서 많이 사용되는 기능입니다. 특정 url로 호출되는 요청을 각 요청 전달 목적지에 맞게 분배하는 역할을 수행하지요. 호스트는 톰캣에도 구현되어 있으며 이를 통해 하나의 톰캣 내에서 같은 컨텍스트를 갖는 요청의 처리가 가능합니다.
팁
톰캣의 프로세스를 서비스 마다 생성하지 못하는 상황(e.g. 자원의 한계와 같은) 또는 서비스에서 소모하는 자원이 크지 않아 추가적인 프로세스를 기동하지 않아도 되는 상황 등 호스트의 기능을 활용 할수 있습니다.
호스트를 관리하기 위해 톰캣에서는 'host manager'를 제공합니다. 앞서 배치에서 보았던 'Manage APP'와 같이 호스트를 쉽게 구성할 수 있도록 UI환경을 제공합니다. 이를 통해 설정파일에 직접 수정하기보다 제공되는 UI로 정확하고 쉽게 구성하고 관리할 수 있도록 합니다.
Host Manager
클릭$CATALINA_HOME/conf/tomcat-user.xml
에 설정하는 방법을 에러페이지에서 확인user/passwd
를 admin/admin
으로 설정)<tomcat-users>
+ <role rolename="manager-gui"/>
+ <role rolename="admin-gui"/>
+ <user username="admin" password="admin" roles="manager-gui,admin-gui"/>
+</tomcat-users>```
+
user/passwd
입력 후 로그인host manager
에서는 기본 호스트 외에 추가적인 호스트에 대한 추가를 할 수 있도록 Add Virtual Host
를 사용할 수 있습니다. Host
디스크립터에서 정의되는 내용을 각 항목에 맞게 입력 할 수 있고 이렇게 추가된 호스트는 Host name
테이블의 commands
에서 개별적으로 시작과 정지가 가능합니다. 호스트가 정지되어 비활성화 된 상태에서는 해당 호스트의 요청 url에 맞게 들어오더라도 기본 호스트가 처리하게 됩니다. host manager
는 웹페이지를 통한 호스트의 추가/삭제/컨트롤이 가능하므로 외부, 또는 관리자가 아닌 사용자가 접근하지 못하도록 해야 합니다.
톰캣 단일로 서비스하는 경우도 있지만 일반적으로 웹서버와 연동하여 사용하는 경우가 보다 더 많습니다. 그 이유를 다음과 같이 정리합니다.
톰캣에서 처리하는 서비스 요청이 증가하면 단일 프로세스로 처리가 부족한 상황이 발생합니다. 처리에 필요한 힙 메모리를 추가해야 한다면 현재 적용된 메모리 설정보다 더 많은 값을 설정하고 CPU 자원이 부족하다면 장비의 교체도 고려해 봐야겠습니다. 이런 접근은 가용한 메모리가 없거나 더 나은 장비를 추가 구입/구성 해야하는 점이 있습니다. 따라서 단일 프로세스의 한계를 유연하게 대처하기 위해 복수의 프로세스에서 동일한 서비스를 구성하는 방안을 고려할 수 있습니다. 그리고 복수의 프로세스에 요청을 전달하기 위해 LB(LoadBalancer)가 필요합니다.
LB 기능을 수행하는 대표적인 두가지는 네트워크 장비(스위치 장비: L2, L4, L7)를 사용하는 방법과 HTTP 요청을 받아 분산이 가능한 웹서버 입니다. 어떤 방법을 사용하던지 복수개의 톰캣을 사용하면 상황에 따라 프로세스를 추가하여 처리하는 용량을 증가시킬 수 있습니다. 물론 앞서 장비의 추가 상황을 제외한다면 단일 프로세스 보다는 복수의 프로세스를 사용하여 부하를 분산시킬 수 있습니다.
톰캣은 웹서버의 역할을 함께 수행할 수 있는 기능을 동반하고 있습니다. 하지만 정적인 소스를 처리함에 있어서는 기존 웹서버의 처리 능력이 더 우월하기 때문에 소스 처리의 추체를 분산시켜 처리 속도를 증가시킬 수 있습니다. 대표적인 정적인 소스는 html, css, 이미지 파일 입니다. 앞서 요청의 분산으로 부하를 분산시키는 역할과 더불어 어플리케이션 소스 또한 처리추체를 분산시키고, 더불어 웹서버와 톰캣에서 좀더 빠르게 처리 할 수 있고, 처리 가능한 요청의 처리를 분담할 수 있습니다.
일반적으로 Failover로 표현하는 장애처리 및 장애극복은 복수의 톰캣 프로세스를 사용함에 따른 장점입니다. 특정 톰캣 프로세스에 장애가 발생하더라도 다른 톰캣 프로세스에서 요청을 처리하게 됨으로 단일 프로세스로 운영할때보다 서비스 지속성에 장점을 갖습니다.
웹서버는 프록시 기능만을 사용하여도 톰캣과의 연동이 가능하나 톰캣으로의 연동을 좀더 긴밀하게 하기 위해 별도의 모듈을 제공합니다. 이는 Tomcat Connector
로 제공되는데 http://tomcat.apache.org의 Document와 Download에서 확인할 수 있습니다. 연동가능한 대표적인 웹서버로는 다음의 웹서버와 모듈이 요구됩니다.
아파치(Apache HTTP Server)는 가장 많이 사용되고 모든 플랫폼을 지원하는 대표적인 웹서버로서 이번 장에서 설명하고자하는 웹서버와의 연동에서 사용하고자 합니다. 기타 웹서버의 경우 톰캣의 토큐먼트 내용을 참고하시기 바랍니다.
아파치가 설치되었다는 가정하에 톰캣을 연동하는 방법은 다음과 같습니다.
유닉스/리눅스/맥의 경우 mod_jk의 소스를 다운받아 아파치의 apxs
와 함께 컴파일하는 과정이 필요합니다. 윈도우에서도 컴파일하여 사용할 수 있으나 비쥬얼 스튜디오가 있어야 컴파일을 할 수 있기 때문에 별도의 바이너리 파일로 제공됩니다. 다운로드페이지에서 플랫폼에 맞는 모듈을 다운받습니다.
유닉스/리눅스/맥의 경우 컴파일을 수행하기위해 아파치의 'apxs'가 필요합니다. 다운받은 소스 압축파일을 풀고 다음과 같이 컴파일 합니다.
$ tar xvfz tomcat-connectors-1.2.40-src.tar.gz
+$ cd ~/Downloads/tomcat-connectors-1.2.40-src/native
+$ ./configure —with-apxs=$APACHE_HOME_DIR/bin/apxs
+$ make
+$ make install
+
컴파일이 완료된 모듈은 자동으로 $APACHE_HOME/modules/mod_jk.so
로 생성됩니다. 윈도우에서는 다운 받은 바이너리 모듈의 압축을 풀어 동일한 디렉토리에 복사하면 됩니다.
생성된 모듈을 아파치에서 사용할 수 있도록 설정하는 작업을 합니다. $APACHE_HOME/conf/httpd.conf
에 설정하거나 별도의 conf
파일을 생성하여 읽게 하여도 됩니다. httpd.conf
에 설정하는 내용은 다음과 같습니다.
LoadModule jk_module modules/mod_jk.so
+
+<IfModule jk_module>
+ JkWorkersFile conf/workers.properties
+ JkLogFile logs/mod_jk.log
+ JkLogLevel info
+ JkMountFile conf/uri.properties
+</IfModule>
+
JkWorkersFile
은 요청을 처리하는 워커, 즉 톰캣을 정의하는 파일을 지정합니다.
JkMountFile
은 워커와 워커가 처리할 요청을 맵핑하는 파일을 지정합니다. JkMount
만으로도 설정이 가능하나 하나의 파일에서 별도로 관리하기 위해서는 해당 파일을 지정하는 것을 권장합니다. httpd.conf
에 JkMount
를 사용하는 경우 다음과 같이 정의할 수 있습니다.
#jsp 파일을 worker1 워커가 처리하는 경우
+JkMount /*.jsp worker1
+
+#server 경로의 요청을 worker2 워커가 처리하는 경우
+JkMount /servlet/* worker2
+
워커는 그 단어의 의미에서도 추측할 수 있듯이 mod_jk에서 지정하는 요청을 처리하는 대상, 즉 톰켓 프로세스를 의미합니다. 워커는 다음과 같은 설정 방식을 따릅니다.
worker.[WORKER_NAME].[TYPE]=[VALUE]
+
설정의 예는 다음과 같습니다.
worker.properties
의 설정 예제는 다음과 같습니다.
해당 설정은 LB로 구성되는 워커를 정의합니다. LB로 구성될 worker1
과 worker2
를 정의합니다.
worker1
은 192.168.0.10
의 ip와 8009
포트로 ajp13
형태의 요청을 받아들이며 lbfactor
는 1입니다.worker2
은 192.168.0.11
의 ip와 8009
포트로 ajp13
형태의 요청을 받아들이며 lbfactor
는 1입니다.loadbalancer
는 LB를 수행하기 위한 워커로 lb
워커 형태입니다.lb
형태의 워커는 LB 대상 워커를 balace_workers
를 정의하여 나열합니다. 예제에서는 worker1
과 worker2
가 대상으로 지정되었습니다.worker1
과 worker2
는 lbfactor
가 같기 때문에 같은 비율로 요청이 전달됩니다.worker.list
에 지정합니다. 예제에서는 loadbalancer
워커를 지정하였습니다.워커의 정의로 요청을 처리할 워커가 준비되었다면 어떤 요청을 전달할지 정의해햐 합니다. 앞서 JkMount
를 사용한 방식은 간단히 설명하였고 여기서는 uri.properties
파일에서 별도로 요청의 처리 맵핑을 관리하도록 하였습니다. JkMountFile
로 지정되는 이 설정 파일은 다음과 같은 설정 방식을 따릅니다.
[URL or FILE_EXTENSION]=[WORKER or WORKER_LIST]
+
이렇게 설정되는 설정 파일의 내용의 예는 다음과 같습니다.
/*.do=worker1
+/*.jsp=worker2
+/servlet/*=loadbalancer
+
JkMount
와 표현방식에 약간의 차이('='의 사용여부)가 있음에 주의하여 설정합니다.
설정이 완료되면 아파치 프로세스를 재기동 합니다. 이후 맵핑한 요청설정에 따라 아파치에 요청을 합니다. jsp파일을 톰캣이 처리하도록 설정하였다면 톰캣에서 요청해보고 url을 아파치로 변경하여 동일하게 요청되는지 확인합니다.
웹서버와 연동하는 주요 기능중 한가지는 장애처리입니다. 일반적으로는 이런 장애처리 동작시 기존 처리중이던 HTTP Session 정보는 장애가 발생한 톰캣이 가지고 있었기 때문에 없어지게 됩니다. 이같은 현상은 기존에 로그인하여 작업을 하던 중 해당 톰캣 프로세스에 문제가 발생하여 다른 톰캣 프로세스로 요청이 넘어가면 로그인 하던 세션이 끊겨 다시금 작업을 수행하는 현상이 발생하는 것을 예로 들수 있습니다.
톰캣에서는 장애처리시의 HTTP Session을 복구하여 지속적인 세션의 유지를 가능하게 하고자 '클러스터' 기능을 제공합니다. 클러스터는 Multicast로 톰캣 프로세스간에 클러스터를 형성하고 멤버로 구성된 톰캣간에 세션을 공유하는 방식입니다.
기능의 활성화는 단순히 server.xml
의 Cluster
디스크립터의 주석을 해제하는 것만으로도 가능합니다.
하지만 동일 장비에서 기동되는 톰캣간이나 서비스가 다른 톰캣이 여럿 기동중인 경우에는 설정값들이 중복되어 톰캣 기동이나 서비스 처리시 문제가 발생할 수 있습니다. 따라서 기본적인 설정 값 외에 별도의 설정들을 적용해야 하는 경우 server.xml
에서 클러스터를 사용하기 위한 디스크립터 위에 설명한 도큐먼트의 내용을 참고해야 합니다.
만약 도큐먼트의 설정들이 너무 많거나 어떻게 적용해야 하는지 이해하기 힘든경우 톰캣 5.5버전의 server.xml
을 참고하시기 바랍니다. 해당 버전에서는 6.0 이후 단순히 한줄로 적용된 Cluster
디스크립터와는 다르게 기본적인 설정과 값이 같이 적용되어 있습니다. 아래 예제는 도큐먼트의 기본 설정에서 가져온 내용입니다.
http://tomcat.apache.org/tomcat-7.0-doc/cluster-howto.html: 기본 설정
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"
+ channelSendOptions="8">
+
+ <Manager className="org.apache.catalina.ha.session.DeltaManager"
+ expireSessionsOnShutdown="false"
+ notifyListenersOnReplication="true"/>
+
+ <Channel className="org.apache.catalina.tribes.group.GroupChannel">
+ <Membership className="org.apache.catalina.tribes.membership.McastService"
+ address="228.0.0.4"
+ port="45564"
+ frequency="500"
+ dropTime="3000"/>
+ <Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
+ address="auto"
+ port="4000"
+ autoBind="100"
+ selectorTimeout="5000"
+ maxThreads="6"/>
+
+ <Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter">
+ <Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/>
+ </Sender>
+ <Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/>
+ <Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatch15Interceptor"/>
+ </Channel>
+
+ <Valve className="org.apache.catalina.ha.tcp.ReplicationValve"
+ filter=""/>
+ <Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve"/>
+
+ <Deployer className="org.apache.catalina.ha.deploy.FarmWarDeployer"
+ tempDir="/tmp/war-temp/"
+ deployDir="/tmp/war-deploy/"
+ watchDir="/tmp/war-listen/"
+ watchEnabled="false"/>
+
+ <ClusterListener className="org.apache.catalina.ha.session.JvmRouteSessionIDBinderListener"/>
+ <ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/>
+</Cluster>
+
앞서 여러개의 톰캣 클러스터를 사용하는 경우 Membership
과 Reciver
디스크립터의 내용에 주의합니다. Membership
은 동일한 설정을 갖는 톰캣 끼리 같은 클러스터 멤버 그룹으로 인지하는 내용으로 멀티캐스트 통신을 수행합니다. Membershop
의 address
와 port
가 동일한 톰캣 프로세스 끼지 클러스터 기능을 수행합니다. Reciver
는 클러스터간의 메시지를 주고받는 역할을 수행하며 TCP 통신을 수행합니다. 따라서 동일한 장비의 톰캣은 Reciver
에서 설정되는 port
에 차이가 있어야 합니다.
설정된 톰캣 클러스터의 기능은 어플리케이션이 세션 복제를 허용하는가의 여부에 따라 동작하게 됩니다. 따라서 어플리케이션의 web.xml
에 복제가능을 활성화하는 디스크립터를 추가합니다.
<web-app>
+ ...
+ <distributeable/>
+ ...
+</web-app>
+
복제 설정이 추가된 어플리케이션이 배치된 톰캣은 기동시 클러스터를 활성화하고 멤버간에 통신을 수행하는 메시지가 로그에 나타납니다.
구성된 클러스터와 어플리케이션은 LB로 구성되어 요청하며 각 톰캣 프로세스는 세션을 공유하기 때문에 하나의 톰캣 프로세스가 종료되더라도 다른 톰캣 프로세스에서 세션을 받아 수행하는 것을 확인할 수 잇습니다.
Thread는 JVM내에 요청된 작업을 동시에 처리하기 위한 작은 cpu라고 볼 수 있습니다. 톰캣에 서비스 처리를 요청하는 경우 해당 요청은 Queue에 쌓여 FIFO로 Thread에 전달되고 Thread에 여유가 있는 경우 Queue에 들어온 요청은 바로 Thread로 전달되어 Queue Length
는 0을 유지하지만 Thread가 모두 사용중이여서 더이상의 요청 처리를 하지 못하는 경우 새로 발생한 요청은 Queue에 쌓이면서 지연이 발생합니다.
Thread가 많을수록 동시에 많은 요청을 처리하기 때문에 작은 Thread 수는 서비스를 지연시키지만 이에 반해 Thread도 자원을 소모하므로 필요이상의 큰 값은 불필요한 JVM의 자원을 소모하게 되고 하나의 프로세스 내의 Thread 수는 톰캣 기준으로 700개 이하로 설정할 것을 권장합니다.
사실상 요청은 지연이 최소화 되어야 하며 지연이 길어질수록 Thread를 점유하여 동시간대에 사용가능한 Thread 수를 줄이므로 적정한 Thread 개수의 설정 상태에서 요청을 더 많이 받고자 한다면 지연에 대한 문제점을 찾는 것을 우선해야 합니다.
쓰레드는 Connector
기준으로 생성됩니다. 따라서 HTTP나 AJP, SSL이 설정된 Connector
마다 다른 쓰레드 수를 설정할 수 있습니다. 또는 하나의 쓰레드 풀을 생성하고 Connector
에서 해당 쓰레드 풀의 쓰레드를 같이 사용하도록 설정할 수도 있습니다.
기본적인 'Connector'는 다음과 같이 설정되어있습니다.
<Connector port="8080" protocol="HTTP/1.1"
+ connectionTimeout="20000" redirectPort="8443" />
+
+<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
+
HTTP나 AJP 프로토콜이 정의된 Connector
는 설정되어 있지는 않지만 기본값으로 최대 쓰레드 200개의 설정을 가지고 있습니다. 쓰레드 관련 설정값은 다음과 같습니다.
Attribute | Description |
---|---|
maxSpareThreads | Idle 상태로 유지할 max thread pool size |
maxThreads | 동시 요청에 의해 Connector가 생성 할 수 있는 최대 request 수 |
minSpareThreads | tomcat을 실행할때 최소로 유지할 Idle Thread 수 |
maxIdleTime | Thread를 유지하는 시간(ms) |
이런 설정 값들로 다시금 정의하면 기본 Connector
를 다음과 같이 설정할 수 있습니다.
<Connector port="8080" protocol="HTTP/1.1"
+ connectionTimeout="20000" redirectPort="8443"
+ maxSpareThreads="5"
+ maxThreads="15"
+ minSpareThreads="10" />
+
+<Connector port="8009" protocol="AJP/1.3" redirectPort="8443"
+ maxSpareThreads="5"
+ maxThreads="15"
+ minSpareThreads="10" />
+
Executor
디스크립터는 Connector
의 쓰레드 설정에 별도의 실행자로 설정하여 동일한 Executor
를 사용하는 Connector
는 같은 쓰레드 풀에서 쓰레드를 사용하도록 하는 기능입니다.
별도의 Connector
를 사용하여 서비스하지만 모두 같은 쓰레드 자원을 사용하기 위함이며 connector
에 executor
라는 설정을 사용하여 공통의 쓰레드 풀을 이용할 수 있습니다. tomcatThreadPool
이라는 이름을 갖는 Executor
와 각 Connector
에 설정하는 예는 다음과 같습니다.
<Executor name="tomcatThreadPool"
+ namePrefix="catalina-exec-"
+ maxThreads="150"
+ minSpareThreads="4"/>
+
+<Connector executor="tomcatThreadPool"
+ port="8080" protocol="HTTP/1.1"
+ connectionTimeout="20000" redirectPort="8443"/>
+
+<Connector executor="tomcatThreadPool"
+ port="8009" protocol="AJP/1.3" redirectPort="8443"/>
+
Executor
에는 name
을 정의하여 다른 Connector
에서 해당 Executor
를 정의할 수 있는 연결고리를 만듭니다. 그리고 쓰레드의 이름을 정의하는 namePrifix
설정으로 다른 쓰레드와 구분할 수 있도록 합니다. 기존 Connector
에 설정하던 쓰레드 관련 설정을 Excutor
에 함으로서 Connector
에 공통 쓰레드 풀을 제공합니다.
쓰레드 덤프는 실행중인 Thread의 종류와 시작점, 실행한 클래스와 메소드 순서, 현재 상태등을 기록하는 JVM의 고유 기능입니다. 쓰레드 덤프로 서비스의 흐름과 서비스 지연시 수행중인 작업, 병목등을 확인할 수 있습니다.
쓰레드 덤프의 시작에는 쓰레드 이름과 쓰레드의 정보가 기록되며 이후 쓰레드 상태에 대해 설명합니다.
트레이스의 읽는 순서는 위가 최근 실행한 클래스와 메소드이기 때문에 아래서부터 위로 읽습니다.
쓰레드 덤프를 발생시키는 법은 다음과 같습니다.
프로세스 ID를 확인
유닉스/리눅스/맥
ps -ef | grep java
+
JDK 5+
$JAVA_HOME/bin/jps
+
쓰레드 덤프 발생
유닉스/리눅스/맥
kill -3 <pid>
+
JDK 5+
$JAVA_HOME/bin/jstack <pid>
+
쓰레드 덤프 확인
catalina.out
파일 확인무엇인가에 대한 모니터링은 그 대상의 상태를 확인하기 위함입니다. 문제가 있는지, 어떤 동작을 하고 있는지, 알아야 할 내용이 있다면 그 사항을 알수 있도록 하는, 즉 대상의 상태를 감시하는 것입니다.
톰캣을 사용하여 서비스를 제공하는 입장에서는 톰캣의 상태를 감시할 수 있어야 합니다. 톰캣의 작업 상태나 자원의 상태, 특정 문제 상황이 발생하는 징조를 파악하는 것입니다. 모니터링을 잘 수행하면 더나은 서비스와 서비스 장애로 인한 손실을 예방할 수 있습니다.
톰캣에는 기본적으로 제공하는 모니터링 툴이 있습니다. 자세하지는 않더라도 필요한 만큼의 정보를 제공합니다.
앞서 어플리케이션의 배치를 통해 알아보았던 Manager APP
에서는 다음의 정보를 확인 할 수 있습니다. 만약 호스트가 여러개라면 해당 Manager APP
어플리케이션을 별도로 배치하여 해당 호스트의 배치 정보를 확인 할 수 있습니다.
Manager APP
에서의 수행 결과 메시지그리고 상단의 Server Status
링크를 통해 이동하면 다음의 정보를 확인 할 수 있습니다.
배치된 어플리케이션의 Session의 숫자에 링크된 페이지에서는 현재 생성된 세션의 정보와 해당 세션을 강제 종료시킬 수 있는 Sessoin Administration
을 제공합니다.
host manager
에서는 다음의 정보를 확인 할 수 있습니다.
톰캣 5.5 버전에서는 admin
어플리케이션이 제공됩니다. 다운로드의 아카이브 사이트에서 확인 가능하며 톰캣을 다운받기위한 버전하위에 apache-tomcat-x.x.xx-admin
이름을 갖는 파일이 있습니다. 여타 WAS에서 제공되는 만큼의 웹 콘솔 UI를 제공하는 관리 툴로서 톰캣 내에 설정과 각 항목의 정보를 파악할 수 있습니다. 다만 톰캣 5.5 이후로는 제공되지 않습니다.
http://code.google.com/p/psi-probe
psi-probe
는 예전에 lambda probe
였으나 현재 구글에서 관리하기 시작한 후로 명칭이 변경되었습니다.
psi-probe
의 공식 웹 사이트의 다운로드 항목에서 파일을 받아 압축을 풀면 probe.war
웹 어플리케이션이 있습니다. 해당 어플리케이션을 톰캣에 배치하면 psi-probe
를 실행할 수 있으며 tomcat-user.xml
에 manager
권한을 갖는 사용자로 접근하게 됩니다.
ip:port/probe'와 같이 톰캣에 요청하면 psi-probe
가 제공하는 톰캣의 모니터링과 관리를 수행하는 어플리케이션을 확인 할 수 있습니다.
앞서 텍스트로만 표현되던 정보들도 보다 보기좋게 제공하고 각 자원이나 설정을 파악하는데 있어서 기본 톰캣 모니터링 툴보다 나은 기능을 제공합니다. 다만 일부 모니터링 항목은 5.5까지를 지원하고 톰캣 8.0에 대한 지원이 불가능하며 2013년 3월 이후로 업데이트가 없다는 점이 단점입니다.
jkstatus는 mod_jk
를 사용하여 연동한 경우 아파치에서 확인할 수 있는 톰켓 연동에 대한 모니터링 툴 입니다.
사용을 위해서 worker.properties
에 status
워커를 추가합니다.
worker.list=tomcat1,tomcat2,loadbalancer,status
+...
+worker.status.type=status
+
그리고 uri.properties
에 요청을 수행할 경로를 워커에 맵핑합니다.
...
+/jkstatus=status
+
설정 후 아파치를 재기동하면 아파치 요청 url의 컨텍스트에 설정한 요청 경로를 입력하여 'jkstatus' 툴을 확인 할 수 있습니다. 앞서 80으로 요청하는 아파치의 경우 다음과 같이 요청 할 수 있습니다.
jkstatus로 확인할 수 있는 정보는 다음과 같습니다.
visualVM은 톰캣만이 아닌 JVM 전반에 대해 모니터링을 제공하는 툴로서 제공됩니다. Oracle JDK 1.6.0_18 버전 이상부터는 기본으로 포함되어 있고 별도의 다운로드를 위해서는 관련 공식 페이지인 'http://visualvm.java.net'을 통해 받을 수 있습니다.
JDK에는 $JAVA_HOMe/bin/jvisualvm
에 실행파일이 위치합니다.
visualVM의 장점중 하나는 플러그인 입니다. 현재까지도 상당수의 플러그인이 제공되고 있으며 플러그인 API가 공개되어 있어 원하는 모니터링 플러그인을 생성할 수도 있습니다.
자바 프로세스는 자체적으로 로컬환경에서는 visualVM에 자동으로 인지되게 됩니다. 따라서 수행중인 JVM 프로세스는 visualVM의 Local
항목에 감지되어 목록에 나타납니다. 그리고 원격지의 JVM 프로세스 또한 JMX(Java Monitoring Extension)을 통해 로컬의 visualVM에서 모니터링 할 수 있습니다. 서비스로 등록된 로컬의 톰캣은 프로세스가 보이지 않기 때문에 리모트로 구성하는 방법을 따릅니다. 톰캣의 리모트 구성 방법은 두가지가 있습니다.
Java 기본 JMX 설정
Java에서는 옵션을 통해 JMX를 활성화하고 설정 할 수 있습니다. 스크립트에 다음의 JMX의 옵션을 설정합니다.
#setenv.sh
+CATALINA_OPTS="-Dcom.sun.management.jmxremote
+-Dcom.sun.management.jmxremote.port=18080
+-Dcom.sun.management.jmxremote.ssl=false
+-Dcom.sun.management.jmxremote.authenticate=false"
+
이 후 visualVM의 'Remote'에 플랫폼 IP를 등록하고 우클릭을 하면 'Add JMX connection...'을 통해 원격지의 톰캣을 등록할 수 있습니다.
jmx remote 모듈
톰캣에서는 jmx를 위한 모듈을 제공합니다. 톰캣의 다운로드에 보면 Extra
항목에 Remote JMX jar
가 있습니다.
$CATALINA_HOME/lib/catalina-jmx-remote.jar
에 위치시킵니다.$CATALINA_HOME/common/lib/catalina-jmx-remote.jar
에 위치시킵니다.Java에서는 옵션을 통해 JMX를 활성화하고 설정 할 수 있습니다. 스크립트에 다음의 JMX의 옵션을 설정합니다.
#setenv.sh
+CATALINA_OPTS="-Dcom.sun.management.jmxremote.ssl=false
+-Dcom.sun.management.jmxremote.authenticate=false"
+
그리고 server.xml
에 Listener
디스크립터로 JmxRemoteLifecycleListener
를 추가합니다.
<Server port="8005" shutdown=“SHUTDOWN">
+
+ <Listener className="org.apache.catalina.mbeans.JmxRemoteLifecycleListener"
+ rmiRegistryPortPlatform="10001" rmiServerPortPlatform="10002" />
+
설정된 톰캣은 visualVM의 Remote 등록시 다음의 정보로 접근 정보를 생성합니다.service:jmx:rmi://192.168.56.101:10002/jndi/rmi://192.168.56.101:10001/jmxrmi
로컬이나 리모트에 설정된 톰캣 프로세스는 좌측의 네비게이션에 나타나며 해당 항목을 더블클릭하여 우측 화면에서 모니터링 하게 됩니다.
visualVM에서는 기본적으로 다음의 기능을 제공합니다.
이외에도 플러그인에서 제공하는 기능을 활용한 다양한 모니터링이 가능합니다.
http://www.oracle.com/technetwork/java/javase/2col/jmc-relnotes-2004763.html
JMC(Java Mision control)은 bea사에서 만든 별도의 JDK인 JRockit에서 제공하던 모니터링 툴입니다. 현재는 bea가 오라클사에서 인수하면서 관련 소프트웨어도 오라클이 관리하고 있으며 관련하여 Sun사도 인수하면서 기존 Sun Hotspot JDK와 JRockit의 장점을 합친 결과로 여러 기능이 추가되고 있습니다. 특히 JDK 7에서 많은 변화가 있었으며 여기에 JMC가 포함되었습니다.
JDK 7 이상의 버전에서 "$JAVA_HOME/bin/jmc"로 실행시키며, 수행된 툴은 다음과 같은 모습을 갖고 있습니다.
visualVM과 거의 비슷한 정도의 모니터링 기능을 제공하는 JMC의 대표적인 특징은 GC 정책에 따른 모니터링 탭의 변화 입니다. GC 정책은 기본 Parellel GC외에도 필요에 따라 CMS(Concurrent Mark Sweep)이나 G1 정책이 사용될 수 있는데 이런 GC 정책에 따른 뷰가 변경됨이 큰 특징입니다.
APM은 Application Performence Manager의 약자로 모니터링의 역할과 더불어 어플리케이션의 성능을 향샹시킬 목적으로 사용되는 별도의 어플리케이션입니다. WAS의 상용 APM 으로는 Jennifer, Pharos, Performizer 등이 있고 Opensource로 Scouter가 대표적입니다.
하나의 장비에는 둘 이상의 톰캣을 운영하는 경우가 발생합니다. 이경우 설정 파일을 별도로 생성하고 스크립트를 통해 해당 설정을 읽게 하는 식의 방법을 사용할 수도 있지만 기본 제공되는 스크립트가 수정되는 양이 많을수록 관리의 난이도가 증가합니다. 따라서 톰캣의 프로세스를 여러개 기동하기 위한 방법으로 권장하는 것을 $CATALINA_HOME
을 단순히 여러개 만드는 것입니다. 톰캣은 그 자체 용량이 그리 크지 않기 때문에 여러개 복사한다하여도 큰 무리 없이 사용가능한 가벼운 엔진 입니다. 따라서 설정 파일을 추가로 구성하고 스크립트를 수정하는 방법 보다는 기본 톰캣 엔진 전체를 복사하는 것을 권장합니다.
앞서 옵션을 적용함에 있어 강조한 setenv를 이야기 하고자 합니다. 톰캣에 대한 서비스를 지원하다보면 주로 catalina.sh(bat)
를 수정하는 경우가 대부분이고 현재가지는 운영중인 서비스에 setenv
사용한 사례찾기는 힘든것 같습니다. 하지만 catalina.sh(bat)
스크립트에서도 명시하듯 해당 스크립트를 수정하는 것은 설정한다는 이유 외에는 단점이 더 많기 때문에 반드시 setenv
를 통해 추가적인 스크립트 추가 설정을 권장합니다.
Unix/Linux/Mac 플랫폼에서 톰캣이 별도의 계정으로 구성되어 있지만 간혹 root 계정으로 실수로 기동하는 경우가 발생합니다. 톰캣에서는 server.xml
에 다음의 설정으로 root 로의 기동을 방지 할 수 있습니다.
<Server port="8005" shutdown="SHUTDOWN">
+ <Listener className="org.apache.catalina.security.SecurityListener" checkedOsUsers="root" />
+ ...
+
이러한 Listener
디스크립터의 설정으로 root 계정의 실행을 방지하며, 만약 root로 기동하는 경우 다음과 같은 메시지를 발생시킵니다.
java.lang.Error: Start attempted while running as user [root]. Running Tomcat as this user has been blocked by the Lifecycle listener org.apache.catalina.security.SecurityListener (usually configured in CATALINA_BASE/conf/server.xml)
+
Connector
디스크립터로 정의되는 프로토콜에 대한 정의는 톰캣이 요청을 받아들이는 통로를 설정하는 것이기 때문에 주요 설정 중 하나 입니다. 앞서 살펴본 쓰레드 설정외에도 도움이 될만한 옵션에 대한 내용은 다음과 같습니다.
옵션 | 기능 설명 |
---|---|
acceptCount="10" | request Queue의 길이를 정의 : idle thread가 없으면 queue에서 idle thread가 생길때 까지 요청을 대기하는 queue의 길이 : 요청을 처리할 수 없는 상황이면 빨리 에러 코드를 클라이언트에게 보내서 에러처리 표시 |
enableLookups="false" | Servlet/JSP 코드 중에서 들어오는 http request에 대한 ip를 조회 하는 명령등이 있을 경우 DNS 이름을 IP주소로 바꾸기 위해서 DNS 서버에 look up 요청을 보냄 : 서버간의 round trip 발생을 막을 수 있음 |
compression="off" | HTTP message body를 gzip 형태로 압축해서 리턴하지 않음 |
maxConnection="8192" | 하나의 톰캣인스턴스가 유지할 수 있는 Connection의 수를 정의 : 현재 연결되어 있는 실제 Connection의 수가 아니라 현재 사용중인 socket fd (file descriptor)의 수 |
maxKeepAliveRequest="1" | HTTP 1.1 Keep Alive Connection을 사용할 때, 최대 유지할 Connection 수를 결정하는 옵션 : Keep Alive를 사용할 환경이 아닌 경우에 설정 |
tcpNoDelay="true" | TCP 프로토콜은 기본적으로 패킷을 보낼때 바로 보내지 않음 : 버퍼사이즈에 데이터가 모두 담길때까지 패킷 전송을 보류함으로 대기 시간이 발생하는 것을 방지 : 트래픽이 증가하지만 현 망 속도를 고려하였을 때 문제가 크지 않음 |
일시 : 2019년 4월 24일 수요일 저녁 19시 ~ 21시
안내 : 컨테이너 연구소 - 컨테이너 시스템의 활용 방향 및 미래에 관련해서 좌담
장소 : 메가존 지하 강연장
자원을 잘 나눠주는 프로세스.
Zip같은 패키지인데 바퀴도 있고 엔진도 있는
개발자들의 공용어
VM이 H/W와의 분리였다면 컨테이너는 OS와 분리
떠나보낸 연인...하지만 사랑한다
일시 : 2024년 2월 13일 (화) 19:00
안내 :
장소 : 파크원2타워 티오더
주요 아젠다
법원의 판결은 기존 판례에 큰 영향을 받는다. 사건마다 그 배경, 상황, 증거, 정황 등의 정보를 기반으로 이전에 어떤 결과가 나왔는지가 현재의 판결문 작성과 연관성이 있다는 것은 정보들의 분석으로 현재의 사건이 어떤 결과가 나올지 예측할 수 있고, 더불어 생성형 AI를 사용하는 소비자 또한 이런 '법률 서비스'를 받는 효과를 얻을 수 있다는 것이다.
관련 뉴스 - 법원도 AI 도입 시작…"판결문 작성 돕는 AI도 필요"
법률과 AI는 이전에 미국에서 ChatGPT로 찾은 자료를 법원에 제출하여 벌금형을 받은 사례도 있지만 그것은 잘못 사용된 예시이자 사실 검증을 해야한다는 경종이기도 하다
패널로 참여하신 파란두루미의 이인희 대표님은 AI에 이전 판례들을 적용하여 주변인의 어렵게만 보이는 법률 싸움(?)에 도움을 준 적이 있었다.
생성형 AI는 전문가적 소견이나 전체 설계는 아직 무리일 수 있지만, 검증, 자료정리, 부분적 구현에 탁월함을 보여준다. (아직이라는 것은 앞으로는 그럴 가능성도 있다는 것이다.) 특히 어떤 정보를 학습시키는가에 따라 원하는 답을 얻을 수 있다.
게임 업계에서는 예술가의 영역이였던 캐릭터 일러스트와 배경 같은 영역에 생성형 AI의 도입이 활발해지고 있다고 했다.
게임 같은 엔터테인먼트 요소가 강한 소프트웨어 산업에서는 눈으로 보여지는 요소가 흥행에 많은 비중을 차지할 수 밖에 없다. 때문에 아티스트에 많은 비용이 지불되고, 또는 유명한 아티스트를 섭외해야하는 경우도 잦았지만, 생성형 AI가 이를 대체한다면 원하는 원화를 얻을 때까지 반복적으로 요청 할 수 있고, 이것은 이전에 사람이 직접 모든 예술적 작업을 수행했던 상황에 비해 상대적으로 적은 비용으로도 게임 제작을 진행할 수 있다는 장점이 있다.
눈으로 보여지고 사용자 경험이 요구되는 프론트엔드 개발, 백엔드 개발자도 할 수 있어요.
전문 영역을 벗어나 다른 영역을 새로 배우기에는 시간과 비용이 필요하지만 생성형 AI를 활용하면, 백엔드 개발자도 스마트폰 앱이나 UI 작업 같은 다른 역량이 필요한 영역에 대해서도 개발 가능한 기회를 얻게 된다.
생성형 AI는 초급자 개발자를 두는 것 같은 효과가 있어요.
결국 명령(프롬프팅)을 하는 사람이 원하는 바를 정확히 전달할 수 있다면 생성형 AI는 그 일을 초급자가 할 수 있을 정도의 결과물을 만들고, 이것은 기존의 전문영역을 벗어나 더 많은 가능성을 열어주는 역할을 수행한다.
생성형 AI를 자체 구축하여 사용하는 경우도 있다. 그 유용성과 업무 효율성을 기대하지만 외부 SaaS 형태를 사용하게 된다면, 기업의 정보가 유출 될 가능성이 있기 때문이다.
채팅하는 것처럼 사용하면, 그게 다 토큰이고 돈이에요.
하지만 생성형 AI이 이제 막 활성화 되듯, 사용자도 아직 노하우가 많은편은 아니다. 질문 하나 당 모두 모델로부터의 결과를 도출하기 위한 비용(전기, 프로세싱)이 발생하는 작업인데, 마치 일반 사용자들은 채팅하듯, 아주적은 정보들로 주거니 받거니 하며 사용하는 사례도 있었다.
대화 한 번에 ‘생수 한 병씩’…챗GPT의 불편한 진실
참석자로 참여하다 패널로 전향(?)하신 삼성SDS의 조남호 프로님의 비용에 대한 언급, 그리고 사용법에 대해 교육했음에도 아직은 제대로 사용하지 못한다는 사실에서 답답함이 느껴지기는 했지만, 한편으로는 기업내부에서 생성형 AI에 대한 올바른 사용법과 활용법에 대해 고민하고 실행하고 있다는 것은 앞으로의 AI와 함께 하는 세상에서 또 다른 기업/개인 경쟁력이 될 것도 같다.
어떻게 하면 올바른 질문을 할 수 있을까에 대한 해법으로는 서로다른 LLM을 사용해보는것도 방안으로 제시되었다. 예를들어, Edge Copilot에게 프롬프트를 만들어달라고 요청하고, 생성된 프롬프트를 ChatGPT에 입력하는 방식이다. 조남호 프로님은 더 나은 사용을 위해서 무료 강의를 들어볼 것을 추천하였다.
LERAN GENERATIVE AI - Short Courses
ChatGPT의 발표 이후로 수많은 관련 서적들이 출간되었고, 더불어 개발자 영역에서도 다양한 활용법, 특히 귀찮은 작업을 시키거나, 사용자에게 비 전문적인 영역의 해답(?)을 얻는 방식이 제안되었다.
Copilot이 생성해준 코드를 붙여넣으면, 설명 할 수 없고 우리의 코드 규칙을 지키기 어려워요.
참석하신 한 CTO분은 개발에 더이상 생성형 AI를 사용하지 않는 것으로 방향을 정했다고 이야해 해주었다. 앞서 다양한 활용법, 긍정적인 부분이 부각되었다면 이것은 반대적 입장이여서 관심이 가는 부분이였다. 특히 당장의 생산성은 높아질 수 있지만, 작성된 코드를 붙여넣은 사람도 설명할 수 없고 협업을 함에 추가적인 노력과 디버깅이 필수적이 되면서 생산성과 조직 거버넌스를 유지시키기 어려운 것이 그 이유 였다.
생성형 AI의 주요 문제점중 하나는 이미 학습된 데이터에 의존하는 편향적 경향이다. 이 문제는 편견이 완화되기보다는 편향된 결과를 더 확대하거나 지속적으로 출력하기 때문에 공개되어있는 학습모델을 그대로 활용하는 것은 작업자의 의도와는 다르게 흘러할 수 있다. 때문에, B2B 서비스를 개발하는 참여자 분은 자주 변경되고 일관성없는 프론트엔드를 관리하고 통합하기 위해 사내 표준 코드들을 학습시키고 이를 활용할 계획을 고려하고 있었다.
티오더의 하담님도 학교에서 생성형 AI와 관련하여 현 개발에서의 분위기, 그리고 미래에는 많은 부분이 사람을(개발자도) 대체할 것이라는 강의를 했고, 지금의 자녀들에게 코딩을 배우게 하는 것이 옳은지에 대한 질문을 받았다고 했다.
미래에 모든것을 AI가 대체한다면, 지금 코딩을 배우는게 의미 없는거 아닌가요?
이런 직업적 위기는 비단 생성형 AI 이전에도 있었다. 산업이 발전하면서 자동화로 인해 로봇이 제조 공정에서 사람이 하던 일, 즉, 물리적 노동력을 대체하는 것에 대한 위기가 있었다. 그리고 이제는 인간의 지식 노동력을 대체하는 것으로 AI가 그 위협으로 다가오고 있다.
인간의 지식 노동 대체하는 인공지능, 시장 경제 뿌리부터 흔들 것
혹자는 살아남을 직종이 창조적인 일, 예술이나 미술이라 이야기 했지만, 이미 대중적이고 일반적인 일을 대체 할 수 있다는 것은 앞선 게임 업계에서의 일로도 충분히 그렇지만은 아닐 것임을 알려주고 있다. 또한 화제가 되었던 테슬라의 로봇이 빨래를 접는 영상에서도 보이듯, 섬세한 운동능력도 기계와 AI가 결합하면서 그 가능성을 보여주고 있다.
Elon Musk’s Latest Robot Video Accidentally Gives Away The Magic Trick
좌담회에서 이런 고민과 걱정을 하기 전부터도 생성형 AI가 사람들의 일자리를 위협하리라는 가설들이 쏟아졌다. 물론 이전 역사를 돌아보아도 기술의 발전으로 없어진 직업들이 있었다. 하지만 새로운 기술 및 그에 따른 새로운 행동 양식은 또 다른 역량을 요구하였고 새로운 직업들이 생겨났다. 심지어 이전에는 중요하지 않았던 직무가 중요해진 경우도 있다.
'프롬프트 엔지니어링' 이라는 용어가 나올정도로 어떤 질문을 던지는가가 생성형 AI를 대하는 요구조건이라고 한다면, 올바른 질문을 던질 수 있는 역량이 중요해질 수도 있다. 때문에, 좌담회에서는 오히려 중급 고급 인력은 유지될테지만 초급 인력의 자리가 위협받을 수 있음에 우려가 있었다.
또한 일 이라는 것은 소통의 연속이고, 배경 지식이 없다면 시장을 파악하고 상대의 의중을 아는데 어려움을 겪는다. 생성형 AI가 모든것을 해줄 것 같지만 아직 전체를 파악하고 통찰을 제공하지는 못한다.(현재로서는...)
좌담회의 주제가 'AI의 시대에서 DevOps가 가야할 방향' 인 것을 고려하면, DevOps를 발전시킴에 있어서 AI는 어떤 작용을 할 것인지 우리는 선택해야 한다. 필자의 예로, 최근 '테라폼으로 시작하는 IaC'라는 책을 냈고, 코드로 인프라를 만든다는 목적의 도구를 다루다보니 안좋은 피드백은 주로 '이건 실무랑 달라,' '내가 원하는건 클라우드를 어떻게 잘 만드는지에 관한건데 그런 내용은 없더라' 같은 내용이였다. 이미 특정 환경을 프로비저닝 한다는 예를 든 책들이 있고, 온라인에 문서에 더 잘 나와있기도 하고, 이번 주제인 AI가 이미 그런 코드를 더 잘 만들기도 하기에 그런 내용은 의도적으로 피하려고도 했다. 오히려 도구가 만들어진 사상과 개발자가 의도한 기능과 방식을 이해하는데 목적을 두었다. 지금은 아는만큼 물을 수 있고, 물어보는 질문의 수준에 따라 답을 얻을 수 있는 AI를 접하는 시기이기 때문이다.
DevOps가 일하는 방식을 고려했을 때 중간에 언급된 'X-LLM'이란 용어도 중요해 보인다. 기존 LLM 모델보다도 작고, 극한 상황에서도 실행되어야 하는 Extreme 이라는 수식이 붙은 이 모델은 엣지, IoT에 연관이 높은 통신에서 큰 관심이 있다. DevOps는 효율적으로 모델을 생성하고 배포하는 일련의 작업을 극한의 환경에 잘 배포하고 업데이트 할지에 대한 고민도 필요할 것이다.
돌아와서, DevOps는 '문화'라 표현하지만 약간 뜬구름 같은 느낌이 있고, 누군가 알려준 '조직간 간극을 줄이는 활동'이라는 것이 더 잘 와닿는다고 생각한다. 앞서 사례에서도 이제는 다른 사람의 도움 없이도 혼자 해볼 수 있는 환경을 제공하기도 하고, 주니어 개발자의 자리도 대신할 수 있다는 가능성도 있지만 DevOps 측면에서 AI를 활용해서 어떻게 조직간의 간극을 줄일 수 있을지의 방안들도 고민해 볼 시기인 것 같다.
기술의 발전으로 디스토피아를 상상할 수도 있지만 유토피아 또한 마찬가지이고, 유토피아는 저절로 오지 않고 '유토피아적'으로 만들려는 노력이 있어야만 할 것이다. 최신의 기술 이지만 뜨거운 감자이기도 한 생성형 AI를 두고 아직은 동상이몽이지만, 결국 돈이 어떻게 흐르는가에 따라 방향성이 정해지지 않을까?
앞서 자체 구축형 AI 인프라를 갖고 있다는 사례에서 다시한번 느끼는 것은 생성형 AI는 비용을 지불하거나 투자 받을 수 있는 조건에서야 가능한 자본 집약적 기술이다는 것이다. NVIDIA의 주가는 치솟는 이유, 그리고 최근 샘 올트먼이 AI 반도체에 투자하겠다고 한 것도 생성형 AI의 판도를 반도체와 그 반도체로 구성된 무엇인가를(현재는 GPU) 좌우 할 수 있는 것이 향후 판도를 가름할 것이라 보인다.
뒷풀이에서 회자된 것 처럼 결국 NVIDIA 주식이 답인가? 🤣
일시 : 2019년 5월 23일 (목) 19:00 ~ 21:30
안내 : 컨테이너 연구소 - 컨테이너 시스템의 활용 방향 및 미래에 관련해서 좌담 part2
장소 : 대륭서초타워 베스핀글로벌
컨테이너는 rootless 하다.
kubernetes는 할렘가 같다
스타트업의 경우 서버를 구축해서 사용하기 보다는 어느정도 서비스 형태로 된 컨테이너 서비스를 사용할 후 구축해보는 것을 추천한다.
KEDA
Podman vs. Docker
https://developers.redhat.com/blog/2019/02/21/podman-and-buildah-for-docker-users/
Docker
단일 프로세스가 단일 실패 지점이 될 수 있습니다.
이 프로세스는 모든 하위 프로세스 (실행중인 컨테이너)를 소유합니다.
상위 프로세스에 문제가 발생하면 컨트롤에서 벗어나는 프로세스가 발생합니다.
컨테이너 환경으로 인해 보안 취약성이 발생할 수 있습니다.
모든 Docker 작업은 동일한 전체 루트 권한을 가진 사용자 (또는 사용자)가 수행해야했습니다.
Podman
Podman 방식은 이미지 레지스트리, 컨테이너 및 이미지 저장소, runC 컨테이너 런타임 프로세스 (no Daemon)를 통해 Linux 커널과 직접 상호 작용하는 것입니다.
기호 | 영어 명칭 |
---|---|
~ | tilde. 이건 미국에서는 "틸다" 쯤 발음한다. 그런데 이게 뭐라 부르는지 모르는 분들 꽤 많음. 그냥 wavy thingy under escape하는 사람도 본 적이 있다. |
` | backtick 또는 backquote |
! | exclamation 또는 exclamation point. IT업계 종사자는 bang이라고도 한다. |
@ | at sign |
# | pound 또는 crosshatch 또는 number sign 또는 hash. 한국에서는 흔히 sharp라고 불리는데 의외로 미국에서는 그렇게 부르는 사람을 거의 못봤다. 아마도 한국에서는 음악교육에서 악보일기를 가르쳐서 그런 것 아닐까 싶다. |
$ | dollar sign |
% | percent sign |
^ | caret 또는 circumflex. 과학용 계산기에 익숙한 사람들은 exponential power sign이라고 하기도 한다. 그런데 미국사람들도 이걸 뭐라는지 모르는 사람이 의외로 많다. 그냥 6키 위에 있는 것(that thing above 6 key)라고 하는 사람들 간혹 만난다. |
& | ampersand 또는 and sign |
* | asterisk 또는 star symbol. |
( | opening parenthesis. 줄여서 open paren만 하는 경우가 흔하다. |
) | closing parenthesis. 줄여서 close paren... |
_ | underscore. 아주 드물게 underbar라고 하는 사람도 있다. |
- | minus sign 또는 hyphen 또는 dash |
+ | plus sign |
= | equal sign |
{ | opening brace . 흔히 left curly brace라고불린다. |
} | closing brace 또는 right curly brace |
[ | opening bracket. 흔히 left square bracket이라고도 불린다. |
] | closing bracket 또는 right square bracket |
| | pipe 또는 vertical bar |
\ | back slash 또는 backward slash |
: | colon |
; | semicolon 발음은 세미콜론 또는 세마이콜론 |
“ | quotation mark 또는 double quote |
‘ | apostrophe 또는 single quote |
< | less than sign. 가끔 left angle bracket이라고 부르는 경우가 있다. |
, | comma |
> | greater than sign. 가끔 right angle bracket이라고 하는 사람들이 있다. |
. | period 또는 dot. 일반적으로 period는 영어 언어적 용도로, dot은 컴퓨터 프로그래밍 관련으로 쓰인다. |
? | question mark |
/ | slash 또는 forward slash. |
Full name definition
List of informationtechnology initialisms
Already downloaded: /Users/gslee/Library/Caches/Homebrew/downloads/b6ccc5a2a602c2af3480bbcf1656bd9844595974ba60501871ac12504508e818--openssl-1.1.1l.tar.gz
+==> Downloading https://ftp.gnu.org/gnu/wget/wget-1.21.2.tar.gz
+
+curl: (60) SSL certificate problem: certificate has expired
+More details here: https://curl.haxx.se/docs/sslcerts.html
+
+curl performs SSL certificate verification by default, using a "bundle"
+ of Certificate Authority (CA) public keys (CA certs). If the default
+ bundle file isn't adequate, you can specify an alternate file
+ using the --cacert option.
+If this HTTPS server uses a certificate signed by a CA represented in
+ the bundle, the certificate verification probably failed due to a
+ problem with the certificate (it might be expired, or the name might
+ not match the domain name in the URL).
+If you'd like to turn off curl's verification of the certificate, use
+ the -k (or --insecure) option.
+HTTPS-proxy has similar options --proxy-cacert and --proxy-insecure.
+Error: wget: Failed to download resource "wget"
+Download failed: https://ftp.gnu.org/gnu/wget/wget-1.21.2.tar.gz
+
원인 : 다운로드를 위한 링크의 인증서가 만료된 경우 brew에서 다운로드를 위해 사용하는 curl에서 인증서 오류 발생
해결방안 :
~/.curlrc
파일에 --insecure
추가HOMEBREW_CURLRC
환경변수를 enable 하여 curl 설치HOMEBREW_CURLRC=1 brew install curl
+
--insecure
를 활성화 ~/.curlrc
파일을 삭제하거나 해당 파일에서 --insecure
삭제HOMEBREW_FORCE_BREWED_CURL
환경변수를 enable 하여 사용HOMEBREW_FORCE_BREWED_CURL=1 brew install curl
+
macOS Ventura 업그레이드 후 wget 실행시 오류 발생
$ wget
+dyld[4414]: Library not loaded: /usr/local/opt/libunistring/lib/libunistring.2.dylib
+ Referenced from: <1ECBA17E-A426-310D-9902-EFF0D9E10532> /usr/local/Cellar/wget/1.21.3/bin/wget
+ Reason: tried: '/usr/local/opt/libunistring/lib/libunistring.2.dylib' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/usr/local/opt/libunistring/lib/libunistring.2.dylib' (no such file), '/usr/local/opt/libunistring/lib/libunistring.2.dylib' (no such file), '/usr/local/lib/libunistring.2.dylib' (no such file), '/usr/lib/libunistring.2.dylib' (no such file, not in dyld cache), '/usr/local/Cellar/libunistring/1.1/lib/libunistring.2.dylib' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/usr/local/Cellar/libunistring/1.1/lib/libunistring.2.dylib' (no such file), '/usr/local/Cellar/libunistring/1.1/lib/libunistring.2.dylib' (no such file), '/usr/local/lib/libunistring.2.dylib' (no such file), '/usr/lib/libunistring.2.dylib' (no such file, not in dyld cache)
+[1] 4414 abort wget
+
brew uninstall --force gettext
실행했으나 오류$ brew uninstall --force gettext
+xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools), missing xcrun at: /Library/Developer/CommandLineTools/usr/bin/xcrun
+Error: Refusing to uninstall /usr/local/Cellar/gettext/0.21.1
+because it is required by cairo, gdk-pixbuf, git, glib, gnupg, gnutls, gobject-introspection, graphviz, gts, harfbuzz, libidn2, librsvg, libslirp, pango, podman, qemu and wget, which are currently installed.
+You can override this and force removal with:
+brew uninstall --ignore-dependencies gettext
+
--ignore-dependencies
추가하여 실행$ brew uninstall --ignore-dependencies gettext
+Uninstalling /usr/local/Cellar/gettext/0.21.1... (1,983 files, 20.6MB)
+
gettext
재설치하면 도중에 실행이 안됐던 wget
도 재설치$ brew install gettext
+...
+==> Upgrading wget
+1.21.3 -> 1.21.3_1
+
+==> Installing dependencies for wget: openssl@3
+==> Installing wget dependency: openssl@3
+==> Pouring openssl@3--3.0.7.ventura.bottle.tar.gz
+🍺 /usr/local/Cellar/openssl@3/3.0.7: 6,454 files, 28.2MB
+==> Installing wget
+==> Pouring wget--1.21.3_1.ventura.bottle.1.tar.gz
+🍺 /usr/local/Cellar/wget/1.21.3_1: 89 files, 4.2MB
+==> Running `brew cleanup wget`...
+Removing: /usr/local/Cellar/wget/1.21.3... (89 files, 4.2MB)
+==> Checking for dependents of upgraded formulae...
+...
+
git
도 업그레이드 후 영향을 받았는지 재설치 필요Error: 'git' must be installed and in your PATH!
+Warning: gettext 0.21.1 is already installed and up-to-date.
+To reinstall 0.21.1, run:
+ brew reinstall gettext
+
brew install git
으로 다시 git
설치
마지막 재설치 요구에 따라 gettext
재설치
$ brew reinstall gettext
+
wget
재실행 시 정상 동작 확인$ wget
+wget: URL 빠짐
+사용법: wget [<옵션>]... [URL]...
+
+자세한 옵션은 `wget --help'를 입력하십시오.
+
aarch64에서 vuepress 실행을 위해 테스트를 하던 도중 node-gyp와 node-sass에 대한 오류를 맞이하게 되었다.
node-sass의 경우 arm환경에 대한 빌드 릴리즈가 없는 관계로 npm install
을 실행하면 다시 빌드를 하게되는데, 이때 node-sass를 빌드하는 과정에서 빌드 실패가 발생함
node환경에서 sass는 css 코드로 변환해주는 스타일 전처리언어이다. c/c++로 되어있는 구성요소로 인해 빠른 빌드 속도를 제공한다.
관련 내용을 찾다보면 Node 버전에 맞는 node-sass를 사용해야 한다고 나오는데 그 이유는 libsass
때문이고 c/c++로 되어있는 해당 라이브러리 특성상 Node 버전과 실행 환경에 종속적이다.
node-sass 지원 환경 (https://github.com/sass/node-sass/releases)
OS Architecture Node Windows x86 & x64 12, 14, 16, 17 OSX x64 12, 14, 16, 17 Linux 64 12, 14, 16, 17 Alpine Linux x64 12, 14, 16, 17 FreeBSD i386 & x64 12, 14
추가로 node-sass는 항후 Dart Sass로 옮겨진다로 안내되어있음
https://github.com/sass/node-sass
stackoverflow에서 기존 node-sass를 사용하고 있는 경우 해당 프로젝트를 Dart Sass로 전환하는 선언 방식을 알려줬다.
https://stackoverflow.com/a/70171361
npm remove node-sass
+npm i node-sass@npm:sass -D
+
package.json
에 다음과 같이 추가된다.
"devDependencies": {
+ "node-sass": "npm:sass@^1.55.0",
+ },
+
기술은 지속적으로 발전하고 시시각각 변화하고 있습니다. 더불어 IT라는 분야도 점점더 세분화되고, 혼자서는 모든것을 아는것은 거의 불가능합니다.
IT 업을 하면서 정리와 스크랩은 일상이 되어가지만 변화를 쫓아가기는 정말 버겁습니다.
하지만 혼자서가 아니라면 어떨까 라는 생각을 합니다.
집단지성 이란 표현이 있듯, 개인보다는 여럿이 만들어가는 노트입니다. 과거에는 이런 노하우가 개인의 자산으로 비밀처럼 감춰두던 지식이였지만 지금은 서로 공유하고, 알리고, 기여하는 것도 의미 있는 시기인것 같습니다.
코딩의 시대에 걸맞게 코드를 관리하듯 노트를 관리하고 기여하면서 커나가는 오픈소스
아닌 오픈노트
, 오픈노하우
입니다.
다수의 기여자에 의해 문서가 생성되므로 글 작성 방식과 문맥이 서로 상이할 수 있습니다. 각 문서의 기여자는 github repo 에서 확인해주세요.
문서는 본인을 위해, 혹은 동료, 또는 누군가를 위해 필요로 할 때 바로 확인 할 수 있는 기술 문서여야 합니다.
적용되어있는 라이선스는 CC BY-NC-ND 4.0 입니다.
docmoa 에 개시되는 모든 기여되는 문서의 저작권, 권리, 책임은 문서를 기여한 저작자 에게 있습니다.
포맷 변경을 포함하여 모든곳에 공유 가능합니다.
본 사이트의 포스팅에 담긴 관점과 의견은 작성자의 개인적인 생각을 바탕으로 작성되었으며 어떠한 내용도 특정 회사나 단체의 공적인 입장을 대변하지 않습니다.
해당 페이지는 github page
에서 VuePress를 기반으로 구성되었습니다. 이외에 검토되었던 항목은 다음과 같습니다.
페이지 구성을 위해 참고한 자료는 다음과 같습니다.
복잡성에 따라 매주 또는 매일 인프라가 점차 변화하고 발전합니다. 테라폼 구성으로 이런 환경을 캡쳐해두는 것은 매우 쉽고 바른 방식입니다.
그럼 테라폼 설정이 어떻게 동작하는지 알아보겠습니다.
우선 첫번째로, 테라폼은 Refresh를 통해 테라폼으로 만들어질 세상이 어떻게 생겼는지 조정합니다. 이를 통해 테라폼 View가 나오고 실제와 어떻게 다른지 비교합니다. VMware나 AWS, Azure, GCP 같은 인프라에 실제 무엇이 실행 중인지 API로 물어보고 각 상태를 확인할 수 있습니다.
플랜은 현재의 상태를 원하는 상태로 구성하는 단계 입니다. 실제 예상되는 무언가를 알려주고 우리는 미리 확인할 수 있습니다. 앞서 정의한 TF Config의 현재와 다른것이 무엇이있고 어떤 변화가 있는지 확인하고 앞으로의 변경점을 예측해주죠.
이렇게 구성하여 우리가 원하는 인프라를 정의하고 생성하고 적용합니다.
Day 2에 들어서서 기존에 없던 로드발란서와 연결되는 DNS구성이나 CDN, 또는 내 VM을 모니터링하고 싶은 시스템에 연결하는 작업이 필요할 수 있습니다.기존에 가지고 있는 구성을 업데이트하고 다시 실행하여 처음의 리소스에 추가로 새로운 것들을 추가합니다. Day 2에 중요한 것은 계속 변화하는 환경에서 변화될 것들만 추적하고 변경할 수 있다는 것입니다. 그리고 더이상 필요하지 않을 때 원래의 상태로 돌아올 수 있습니다.
코드로 정의된 각 인프라 리소스는 Destroy를 통해 다시 제로의 상태로 돌아옵니다. 이같은 방식은 언제나 일관된 상태와 리소스 정의를 만들어줍니다.
프로바이더는 테라폼 코어와 연동되는 플러그인으로, 각 플랫폼에서 제공하거나 누구나 개발해서 테라폼과 연결할 수 있습니다. 이런 플러그인들을 프로바이더라고 부릅니다.
프로바이더는 필요에 따라 인프라, 플랫폼, 서비스를 연계해서 사용하게도 가능합니다. 예를들어 앞서 인프라를 수행했다고 보고, Day2 에 쿠버네티스 연결이 필요하면 기존 설정을 확장해서 추가 리소스와 자원을 구성합니다. 그리고 그 위에 서비스가 DNS를 요구하거나 CDN을 요구하면 이런 서비스를 추가로 애드온 하게 됩니다.
',9),x={href:"https://www.terraform.io/docs/providers/index.html",target:"_blank",rel:"noopener noreferrer"},B=e("h2",{id:"_3-workflow",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#_3-workflow"},[e("span",null,"3. Workflow")])],-1),b=e("figure",null,[e("img",{src:"https://raw.githubusercontent.com/Great-Stone/images/master/uPic/M-XWeZGoGvexWM5BJwzU5WqAgsol63APP4dlm0iBh_XADq8xGJetiTCAgEbk0LXWDaU83cgGu0l2mh0rtBnsAySYA_j80j1W40Ug01iZSy2CtY7Xr6MV90OM2zQVOnlQU5p8iObm6-I.png",alt:"tf-features_consistent-workflow.png",tabindex:"0",loading:"lazy"}),e("figcaption",null,"tf-features_consistent-workflow.png")],-1),A=e("p",null,"테라폼을 로컬에서 사용하는 사용자의 워크 플로우는 테라폼 구성을 실행하고나서 plan이 만들어집니다. 구성에 대한 State 관리나 변수, 각각의 설정과 관련한 코드를 로컬에서 관리됩니다.",-1),D=e("p",null,"이제 다른 팀원을 추가합니다. 우리는 인프라 작업이 일관되게 변화하는 것을 기대합니다. 새로운 VM 이 생기거나 새로운 리소스를 생기지 않도록 하려면 어떻게 해야할까요? 이런 문제는 우리가 코드 관리를 위해 코드 버전 관리 서비스를 사용하는 현상과 유사합니다. Git기반의 테라폼은 코드를 중앙에서 관리하고 이를 테라폼 엔터프라이즈에서 관리하고 워크플로우의 헛점이 발생하지 않도록 도와줍니다.",-1),k={href:"https://www.terraform.io/docs/cloud/vcs/index.html",target:"_blank",rel:"noopener noreferrer"},G={href:"https://www.terraform.io/docs/cloud/sentinel/index.html",target:"_blank",rel:"noopener noreferrer"},v=e("p",null,"또한가지, 인프라를 구성하는 작업자는 혼자서 운영할 때는 필요한 변수들을 로컬에 저장하고 활용합니다. 이런 변수에는 키같은 민감한 정보토 포함될 수 있습니다. 엔터프라이즈에서는 변수를 중앙에서 관리하고 필요한 경우 암호화 해주는 기능이 필요합니다.",-1),P=e("figure",null,[e("img",{src:"https://raw.githubusercontent.com/Great-Stone/images/master/uPic/81GxnfKd6RWAjGcWyAT_XA5FebyDXpcVcZXwjc215cOnYBeUcVcazui7JkcTqFUkpTcgYvRCSel9HKDFYGLW7FbvxVIWdWUo2ee6ykuCLxn6eUitcIxB9BrY6VJBySb_fl8YSXZov-I.png",alt:"Google Shape;5711;p420",tabindex:"0",loading:"lazy"}),e("figcaption",null,"Google Shape;5711;p420")],-1),S=e("p",null,"이런 엔터프라이즈 기능이 워크 플로우를 관리하고 작업자가 안전하게 협업할 수 있게 도와줍니다.이렇게 반복적으로 작업이 되다 보면 일련을 동작을 모듈화하여 관리할 수 있습니다. 일종의 입출력 예제와 같이 모듈을 쉽게 정의 할 수 있습니다. 예를 들어 Java애플리케이션을 배포하고자 하는 모듈에는 배포할 Jar파일이 무엇인지, 몇개나 띄울지 물어봅니다. 이런 모듈을 기업내에서 관리하는 프라이빗 레지스트리도 기업환경에서는 요구되곤 합니다.",-1),T=e("p",null,"테라폼만으로 좋은데 엔터프라이는 왜 사용하는가에 대한 대답은 이런 협업과 정책, 관리되는 static한 변수들과 해당 조직이나 기업을 위한 모듈을 관리할 수 있도록 만들어준다는 점입니다.",-1);function W(V,X){const r=i("ExternalLinkIcon");return l(),s("div",null,[p,h,m,d,u,g,e("p",null,[t("일련의 선언적으로 구성파일을 정의하고 읽을수 있는 이런 구성파일로 토폴로지를 구성합니다. "),e("a",f,[t("테라폼 config"),o(r)]),t("라고 설명해놓겠습니다. 보안 그룹 규칙을 프로비저닝하거나 네트워크 보안을 설정한 다음 해당 환경 내에서 가상 머신 세트 정의를 프로비저닝하려는 VM이 있고 로드밸런서가 있고...")]),_,e("p",null,[t("Apply는 원하는 상태를 만들기 위해 실행을 하는 단계 입니다. 필요한 것이 무엇인지 어떤것이 정의되었는지를 말이지요. 예를 들면 VM을 생성하기 전에 보안그룹을 정의하는 것 같은 순차적인 것이 무엇인지 병렬로 진행하는 것이 무엇인지를 이미 알고 있습니다. "),e("a",y,[t("그래프 이론"),o(r)]),t("에 기반한 이런 프로비저닝 방식은 사용자가 각 자원의 선후 관계를 명시하지 않아도 순차로 진행할 것과 병렬로 진행할 작업을 구분합니다.")]),w,e("p",null,[e("a",x,[t("테라폼 문서"),o(r)]),t("에보면 이미 100여개 이상의 프로바이더가 존재하고 커뮤니티의 프로바이더 까지 합치면 테라폼으로 관리가능한 코드기반 자원들은 무궁무진합니다.")]),B,b,A,D,e("p",null,[t("테라폼 엔터프라이즈는 "),e("a",k,[t("VCS와 연동"),o(r)]),t("하여 개인 로컬 환경이 아닌 중앙에서 프로비저닝을 하도록 관리하는 역할을 합니다. 이제 로컬에서 실행하는 대신 제어시스템, github나 빗버킷이나 깃랩같은 VCS를 활용하여 중앙에서 상태관리를 합니다.")]),e("p",null,[t("기업의 운영 환경에서 요구되는 건 또 무엇이 있을까요? 아마도 "),e("a",G,[t("정책"),o(r)]),t("이 필요할 것입니다. 예를 들면 태깅을 해야한다거나 특정 리전에만 프로비저닝을 해야하는 조건을 달 수 있습니다.")]),v,P,S,T])}const M=n(c,[["render",W],["__file","00-introduction.html.vue"]]),z=JSON.parse('{"path":"/04-HashiCorp/03-Terraform/01-Information/00-introduction.html","title":"Terraform 개념 소개","lang":"ko-KR","frontmatter":{"description":"테라폼 소개","tag":["terraform","IaC"],"head":[["meta",{"property":"og:url","content":"https://docmoa.github.io/04-HashiCorp/03-Terraform/01-Information/00-introduction.html"}],["meta",{"property":"og:site_name","content":"docmoa"}],["meta",{"property":"og:title","content":"Terraform 개념 소개"}],["meta",{"property":"og:description","content":"테라폼 소개"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://raw.githubusercontent.com/Great-Stone/images/master/uPic/PrGKxouOBWKZPAyp80ByMMnBlDDBCTJwBJpQA3APXwkoKhmjFUKWp-Ncc60TGNB6XNYEYhxBH6r3HFyEtNBeamu_DxAuRAtcG_3XEqyBH1g4pB6eufVZqwRJELzz8LEoR7xM8qU-BQs-20200701002631005.png"}],["meta",{"property":"og:locale","content":"ko-KR"}],["meta",{"property":"og:updated_time","content":"2023-09-18T13:12:54.000Z"}],["meta",{"name":"twitter:card","content":"summary_large_image"}],["meta",{"name":"twitter:image:alt","content":"Terraform 개념 소개"}],["meta",{"property":"article:tag","content":"terraform"}],["meta",{"property":"article:tag","content":"IaC"}],["meta",{"property":"article:modified_time","content":"2023-09-18T13:12:54.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Terraform 개념 소개\\",\\"image\\":[\\"https://raw.githubusercontent.com/Great-Stone/images/master/uPic/PrGKxouOBWKZPAyp80ByMMnBlDDBCTJwBJpQA3APXwkoKhmjFUKWp-Ncc60TGNB6XNYEYhxBH6r3HFyEtNBeamu_DxAuRAtcG_3XEqyBH1g4pB6eufVZqwRJELzz8LEoR7xM8qU-BQs-20200701002631005.png\\",\\"https://raw.githubusercontent.com/Great-Stone/images/master/uPic/YwvyuWTzXp2ZSKimOCvPaYP7GEU-AjWmCn1r3lr43BGW0zX_51LxzgU8DJkukvL5Ri5McV8FYBPgxn0jYGt0XJLNGDRTz0Af7TkUOD26xBTRxW1QZyFaAMqCKF24qS7zvkTwyIJ6d4s.png\\",\\"https://raw.githubusercontent.com/Great-Stone/images/master/uPic/M-XWeZGoGvexWM5BJwzU5WqAgsol63APP4dlm0iBh_XADq8xGJetiTCAgEbk0LXWDaU83cgGu0l2mh0rtBnsAySYA_j80j1W40Ug01iZSy2CtY7Xr6MV90OM2zQVOnlQU5p8iObm6-I.png\\",\\"https://raw.githubusercontent.com/Great-Stone/images/master/uPic/81GxnfKd6RWAjGcWyAT_XA5FebyDXpcVcZXwjc215cOnYBeUcVcazui7JkcTqFUkpTcgYvRCSel9HKDFYGLW7FbvxVIWdWUo2ee6ykuCLxn6eUitcIxB9BrY6VJBySb_fl8YSXZov-I.png\\"],\\"dateModified\\":\\"2023-09-18T13:12:54.000Z\\",\\"author\\":[]}"]]},"headers":[{"level":2,"title":"1. Provision","slug":"_1-provision","link":"#_1-provision","children":[{"level":3,"title":"Refresh","slug":"refresh","link":"#refresh","children":[]},{"level":3,"title":"Plan","slug":"plan","link":"#plan","children":[]},{"level":3,"title":"Apply","slug":"apply","link":"#apply","children":[]},{"level":3,"title":"Destory","slug":"destory","link":"#destory","children":[]}]},{"level":2,"title":"2. Providers","slug":"_2-providers","link":"#_2-providers","children":[]},{"level":2,"title":"3. Workflow","slug":"_3-workflow","link":"#_3-workflow","children":[]}],"git":{"createdTime":1640262000000,"updatedTime":1695042774000,"contributors":[{"name":"Administrator","email":"admin@example.com","commits":1},{"name":"Great-Stone","email":"hahohh@gmail.com","commits":1}]},"readingTime":{"minutes":0.69,"words":207},"filePathRelative":"04-HashiCorp/03-Terraform/01-Information/00-introduction.md","localizedDate":"2021년 12월 23일","excerpt":"\\n\\n프로비저닝과 관련하여 우리는 Day 0부터 Day 2까지의 여정이 있습니다.
\\nUpdate at 31 Jul, 2019
Jenkins Pipeline 을 구성하기 위해 VM 환경에서 Jenkins와 관련 Echo System을 구성합니다. 각 Product의 버전은 문서를 작성하는 시점에서의 최신 버전을 위주로 다운로드 및 설치되었습니다. 구성 기반 환경 및 버전은 필요에 따라 변경 가능합니다.
Category | Name | Version |
---|---|---|
VM | VirtualBox | 6.0.10 |
OS | Red Hat Enterprise Linux | 8.0.0 |
JDK | Red Hat OpenJDK | 1.8.222 |
Jenkins | Jenkins rpm | 2.176.2 |
Jenkins를 실행 및 구성하기위한 OS와 JDK가 준비되었다는 가정 하에 진행합니다. 필요 JDK 버전 정보는 다음과 같습니다.
필요 JDK를 설치합니다.
$ subscription-manager repos --enable=rhel-8-for-x86_64-baseos-rpms --enable=rhel-8-for-x86_64-appstream-rpms
+
+### Java JDK 8 ###
+$ yum -y install java-1.8.0-openjdk-devel
+
+### Check JDK version ###
+$ java -version
+openjdk version "1.8.0_222"
+OpenJDK Runtime Environment (build 1.8.0_222-b10)
+OpenJDK 64-Bit Server VM (build 25.222-b10, mixed mode)
+
Red Hatsu/Fedora/CentOS 환경에서의 Jenkins 다운로드 및 실행은 다음의 과정을 수행합니다.
`,10),c={href:"https://pkg.jenkins.io/redhat-stable/",target:"_blank",rel:"noopener noreferrer"},m=t(`repository를 등록합니다.
$ sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo
+$ sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key
+
작성일 기준 LTS 버전인 2.176.2
버전을 설치합니다.
$ yum -y install jenkins
+
패키지로 설치된 Jenkins의 설정파일은 /etc/sysconfig/jenkins
에 있습니다. 해당 파일에서 실행시 활성화되는 포트 같은 설정을 변경할 수 있습니다.
## Type: integer(0:65535)
+## Default: 8080
+## ServiceRestart: jenkins
+#
+# Port Jenkins is listening on.
+# Set to -1 to disable
+#
+JENKINS_PORT="8080"
+
외부 접속을 위해 Jenkins에서 사용할 포트를 방화벽에서 열어줍니다.
$ firewall-cmd --permanent --add-port=8080/tcp
+$ firewall-cmd --reload
+
서비스를 부팅시 실행하도록 활성화하고 Jenkins를 시작합니다.
$ systemctl enable jenkins
+$ systemctl start jenkins
+
실행 후 브라우저로 접속하면 Jenkins가 준비중입니다. 준비가 끝나면 Unlock Jenkins
페이지가 나오고 /var/lib/jenkins/secrets/initialAdminPassword
의 값을 입력하는 과정을 설명합니다. 해당 파일에 있는 토큰 복사하여 붙여넣습니다.
이후 과정은 Install suggested plugins
를 클릭하여 기본 플러그인을 설치하여 진행합니다. 경우에 따라 Select plugins to install
을 선택하여 플러그인을 지정하여 설치할 수 있습니다.
플러그인 설치 과정을 선택하여 진행하면 Getting Started
화면으로 전환되어 플러그인 설치가 진행됩니다.
설치 후 기본 Admin User
를 생성하고, 접속 Url을 확인 후 설치과정을 종료합니다.
진행되는 실습에서는 일부 GitHub를 SCM으로 연동합니다. 원활한 진행을 위해 GitHub계정을 생성해주세요. 또는 별개의 Git 서버를 구축하여 사용할 수도 있습니다.
Jenkins는 간단히 테마와 회사 CI를 적용할 수 있는 플러그인이 제공됩니다.
Jenkins 관리
로 이동하여 플러그인 관리
를 클릭합니다.
설치 가능
탭을 클릭하고 상단의 검색에 theme
를 입력하면 Login Theme
와 Simple Theme
를 확인 할 수 있습니다. 둘 모두 설치합니다.
로그아웃을 하면 로그인 페이지가 변경된 것을 확인 할 수 있습니다.
기본 Jenkins 테마를 변경하기 위해서는 다음의 과정을 수행합니다.
`,17),u={href:"http://afonsof.com/jenkins-material-theme/",target:"_blank",rel:"noopener noreferrer"},k=t("Build your own theme with a company logo!
에서 색상과 로고를 업로드 합니다.
DOWNLOAD YOUR THEME!
버튼을 클릭하면 CSS파일이 다운됩니다.
Jenkins 관리
로 이동하여 시스템 설정
를 클릭합니다.
Theme
항목의 Theme elements
의 드롭다운 항목에서 Extra CSS
를 클릭하고 앞서 다운받은 CSS파일의 내용을 붙여넣고 설정을 저장하면 적용된 테마를 확인할 수 있습니다.
\\n\\nUpdate at 31 Jul, 2019
\\n
Jenkins Pipeline 을 구성하기 위해 VM 환경에서 Jenkins와 관련 Echo System을 구성합니다. 각 Product의 버전은 문서를 작성하는 시점에서의 최신 버전을 위주로 다운로드 및 설치되었습니다. 구성 기반 환경 및 버전은 필요에 따라 변경 가능합니다.
\\nCategory | \\nName | \\nVersion | \\n
---|---|---|
VM | \\nVirtualBox | \\n6.0.10 | \\n
OS | \\nRed Hat Enterprise Linux | \\n8.0.0 | \\n
JDK | \\nRed Hat OpenJDK | \\n1.8.222 | \\n
Jenkins | \\nJenkins rpm | \\n2.176.2 | \\n
이 과정은 IaC 도구인 Terraform을 사용하여 클라우드 리소스를 생성하는 실습(Hands-on)과정입니다.
\\n💻 표시는 실제 실습을 수행하는 단계 입니다.
\\n사전 준비 사항
\\n컨텐츠
\\nplan
apply
destroy
\\n기술은 지속적으로 발전하고 시시각각 변화하고 있습니다. 더불어 IT라는 분야도 점점더 세분화되고, 혼자서는 모든것을 아는것은 거의 불가능합니다.
IT 업을 하면서 정리와 스크랩은 일상이 되어가지만 변화를 쫓아가기는 정말 버겁습니다.
하지만 혼자서가 아니라면 어떨까 라는 생각을 합니다.
집단지성 이란 표현이 있듯, 개인보다는 여럿이 만들어가는 노트입니다. 과거에는 이런 노하우가 개인의 자산으로 비밀처럼 감춰두던 지식이였지만 지금은 서로 공유하고, 알리고, 기여하는 것도 의미 있는 시기인것 같습니다.
코딩의 시대에 걸맞게 코드를 관리하듯 노트를 관리하고 기여하면서 커나가는 오픈소스
아닌 오픈노트
, 오픈노하우
입니다.
docmoa 에 개시되는 모든 기여되는 문서의 저작권, 권리, 책임은 문서를 기여한 저작자 에게 있습니다.
포맷 변경을 포함하여 모든곳에 공유 가능합니다.
본 사이트의 포스팅에 담긴 관점과 의견은 작성자의 개인적인 생각을 바탕으로 작성되었으며 어떠한 내용도 특정 회사나 단체의 공적인 입장을 대변하지 않습니다.
기술은 지속적으로 발전하고 시시각각 변화하고 있습니다. 더불어 IT라는 분야도 점점더 세분화되고, 혼자서는 모든것을 아는것은 거의 불가능합니다.
\\nIT 업을 하면서 정리와 스크랩은 일상이 되어가지만 변화를 쫓아가기는 정말 버겁습니다.
하지만 혼자서가 아니라면 어떨까 라는 생각을 합니다.
\\n집단지성 이란 표현이 있듯, 개인보다는 여럿이 만들어가는 노트입니다. 과거에는 이런 노하우가 개인의 자산으로 비밀처럼 감춰두던 지식이였지만 지금은 서로 공유하고, 알리고, 기여하는 것도 의미 있는 시기인것 같습니다.
다운받은 Consul 바이너리를 통해 Gossip 암호화 키를 생성합니다
kubectl create secret generic consul-gossip-encryption-key --from-literal=key=$(consul keygen)
+
발급받은 라이선스 파일을 저장(e.g. consul.hclic)하고 Kubernetes의 secret으로 적용합니다.
kubectl create secret generic license --from-file='key=./consul.hclic'
+
Helm repo add & update
helm repo add hashicorp https://helm.releases.hashicorp.com && \\
+helm repo update
+
global:
+ enabled: true
+ name: consul
+ image: hashicorp/consul-enterprise:1.11.3-ent
+ enableConsulNamespaces: true
+ adminPartitions:
+ enabled: false
+ datacenter: dc1
+ # enterpriseLicense:
+ # secretName: license
+ # secretKey: key
+ gossipEncryption:
+ secretName: consul-gossip-encryption-key
+ secretKey: key
+ tls:
+ enabled: false
+ enableAutoEncrypt: true
+ enableConsulNamespaces: true
+
+client:
+ enabled: true
+ grpc: true
+
+connectInject:
+ enabled: true
+ replicas: 2
+
+dns:
+ enabled: true
+
+controller:
+ enabled: true
+
+syncCatalog:
+ enabled: true
+ toConsul: false
+ consulNamespaces:
+ mirroringK8S: true
+
+
kubectl config use-context $(grep gs-cluster-0 KCONFIG.txt)
+helm install consul -f ./values.yaml hashicorp/consul --version v0.40.0 --debug
+
kubectl port-forward service/consul-server 8500:8500
+
팁
\\n실습을 위한 조건은 다음과 같습니다.
\\n톰캣을 사용하는 첫번째 이유는 기능적인 이유일 것입니다. 즉, 톰캣이 수행하는 역할인 JSP/Servlet 엔진으로서의 역할이겠지요. 톰캣은 세계적으로 가장 많은 Java 기반의 웹어플리케이션 플랫폼으로 사용되고 있습니다. 많은 개발자들이 자신의 첫번째 Java 웹 어플리케이션의 JSP/Servlet 엔진으로 선택하고 있고 실제 운영환경에서도 사용하기에 상당한 양의 노하우를 쉽게 접할 수 있다는 장점이 있습니다.
두번째로는 아마도 무료로 사용할 수 있다는 점이 톰캣을 사용하게 되는 이유일 것입니다. 수많은 벤더사에서 JSP/Servlet 엔진과 추가로 Java Enterprise 기능을 사용할 수 있는 세련되고 검증된 자신만의 제품을 개발하고 상용화 하고 있습니다. 하지만 이러한 제품은 비용이 추가된다는 (큰)고민이 생깁니다. 물론 유지보수 계약을 통해 기술지원과 더불어 든든한 책임전가의 대상(?)인 벤더사가 존재한다는 장점이 있지만 모든 사용자가 이런 비용을 지불할 수 있는것은 아니겠지요.
2020년 JRebel 자료
JRebel에서 언급한것과 같이 Spring Boot, Docker, Hybris 및 AWS와 같은 다른 주요 Java 플랫폼과의 호환성이 뒷받침이 되는것 같습니다.
톰캣의 정식 명칭은 Apache Tomcat Server 입니다. 이를 줄여 톰캣 Tomcat 이라고 흔히 불려지고 있지요. 톰캣의 간단한 이력은 다음과 같습니다.
기준 | 2021년 12월 23일 |
---|---|
Deveoloper | Apache Software Foundation |
Last Stable release | 10.0.14 |
Development Status | Active |
Written in | Java |
Operating System | Cross-Platform |
Type | Servlet Container / HTTP Web Server |
License | Apache License 2.0 |
Website | tomcat.apache.org |
현재까지 출시된 버전은 10.0.14 버전이고 앞으로도 지속적으로 업데이트가 있을 예정입니다. 무료로 제공되는 이유로 인해 톰캣을 기반으로 한 소프트웨어들이 있는데, 이경우 개발된 시점을 기준으로 톰캣 버전이 고정되어 있는 경우가 있습니다. 4.0 버전은 최근 보이지 않지만 JDK 1.4.2 를 사용하던 시기가 가장 많은 Java 웹 어플리케이션이 개발되던 시기이기에 톰캣 5.5 버전은 아직도 상당히 많은 사용처가 있을것이라 보여집니다.
톰캣을 구성하는 핵심적인 요소는 다음의 세가지 컴포넌트입니다.
Catalina는 아마도 톰캣을 사용하면 가장 많이 보게되는 단어 중 하나일 것입니다.
처리되는 컴포넌트의 역할을 이해한다면 톰캣에서 어플리케이션 수행시 발생되는 코드 스텍을 이해하는데 도움이 될 수 있습니다.
톰캣에 대하여 흔히들 '톰캣은 완전한 WAS(Web Application Server)가 아니다'라고 합니다. 앞서 설명하였지만 톰캣은 JSP/Servlet 엔진의 역할을 수행합니다. 하지만 Java Enterprise 기능인 EJB, JTA, JMS, WebService 등은 포함되어 있지 않죠. 이러한 이유로 WAS의 일부 기능만을 수행 할 수 있을 뿐 WAS는 아니다 라고 합니다. 이러한 Enterprise 요소를 지원하기위한 요구사항으로 OpenEJB나 Apache ActiveMQ, Apache CXF등의 컴포넌트 요소가 톰캣과는 별도의 프로젝트로 진행되어 해당 컴포넌트들을 결합함으로 톰캣에서 이를 지원할 수 있었습니다. 이제는 어느정도 시간이 지나 Enterprise 컴포넌트와의 연계성이 뚜렷해졌고 통합이 가능해짐에 따라 톰캣을 Java Enterprise 스펙에 맞게 재 조정하는 프로젝트가 시작됩니다. 이를 TomEE(Tomcat Enterprise Edition)이라 합니다.
',16),b={href:"http://tomee.apache.org",target:"_blank",rel:"noopener noreferrer"},v={href:"https://tomee.apache.org/comparison.html",target:"_blank",rel:"noopener noreferrer"},E=d('Tomcat | TomEE WebProfile | TomEE MicroProfile | TomEE Plume | TomEE Plus | |
---|---|---|---|---|---|
Jakarta Annotations | ✔︎ | ✔︎ | ✔︎ | ✔︎ | ✔︎ |
Jakarta Debugging Support for Other Languages | ✔︎ | ✔︎ | ✔︎ | ✔︎ | ✔︎ |
Jakarta Security (Java EE Enterprise Security) | ✔︎ | ✔︎ | ✔︎ | ✔︎ | ✔︎ |
Jakarta Server Pages (JSP) | ✔︎ | ✔︎ | ✔︎ | ✔︎ | ✔︎ |
Jakarta Servlet | ✔︎ | ✔︎ | ✔︎ | ✔︎ | ✔︎ |
Jakarta Standard Tag Library (JSTL) | ✔︎ | ✔︎ | ✔︎ | ✔︎ | ✔︎ |
Jakarta WebSocket | ✔︎ | ✔︎ | ✔︎ | ✔︎ | ✔︎ |
Jakarta Expression Language (EL) | ✔︎ | ✔︎ | ✔︎ | ✔︎ | ✔︎ |
Jakarta Activation | ✔︎ | ✔︎ | ✔︎ | ✔︎ | |
Jakarta Bean Validation | ✔︎ | ✔︎ | ✔︎ | ✔︎ | |
Jakarta Contexts and Dependency Injection (CDI) | ✔︎ | ✔︎ | ✔︎ | ✔︎ | |
Jakarta Dependency Injection (@Inject) | ✔︎ | ✔︎ | ✔︎ | ✔︎ | |
Jakarta Enterprise Beans (EJB) | ✔︎ | ✔︎ | ✔︎ | ✔︎ | |
Jakarta Interceptors | ✔︎ | ✔︎ | ✔︎ | ✔︎ | |
Jakarta JSON Binding (JSON-B) | ✔︎ | ✔︎ | ✔︎ | ✔︎ | |
Jakarta JSON Processing (JSON-P) | ✔︎ | ✔︎ | ✔︎ | ✔︎ | |
Jakarta Mail (JavaMail) | ✔︎ | ✔︎ | ✔︎ | ✔︎ | |
Jakarta Managed Beans | ✔︎ | ✔︎ | ✔︎ | ✔︎ | |
Jakarta Persistence (JPA) | ✔︎ | ✔︎ | ✔︎ | ✔︎ | |
Jakarta RESTful Web Services (JAX-RS) | ✔︎ | ✔︎ | ✔︎ | ✔︎ | |
Jakarta Server Faces (JSF) | ✔︎ | ✔︎ | ✔︎ | ✔︎ | |
Jakarta Transactions (JTA) | ✔︎ | ✔︎ | ✔︎ | ✔︎ | |
Jakarta XML Binding (JAXB) | ✔︎ | ✔︎ | ✔︎ | ✔︎ | |
Jakarta Authentication (JAAS) | ✔︎ | ✔︎ | |||
Jakarta Authorization (JACC) | ✔︎ | ✔︎ | |||
Jakarta Concurrency | ✔︎ | ✔︎ | |||
Jakarta Connectors | ✔︎ | ✔︎ | |||
Jakarta Enterprise Web Services | ✔︎ | ✔︎ | |||
Jakarta Messaging (JMS) | ✔︎ | ✔︎ | |||
Jakarta SOAP with Attachments | ✔︎ | ✔︎ | |||
Jakarta Web Services Metadata | ✔︎ | ✔︎ | |||
Jakarta XML Web Services (JAX-WS) | ✔︎ | ✔︎ | |||
Jakarta Batch (JBatch) | ✔︎ |
현재 국내의 개발 환경이나 운영 환경에서는 이전에 잠시 EJB가 사용되었을 뿐 최근 전자정부 프레임워크에서도 보이듯 Enterprise 환경을 요구하는 상황은 거의 없는 것으로 보입니다. 아무래도 WAS 의존성을 낮추기 위해서는 벤더에서 주도하여 각각의 컴포넌트를 제공하는 Enterprise 구성 요소에 대한 의존도를 낮출 수 있는 방향이기는 하겠지요. 하지만 일부의 경우는 Enterprise 구성 요소를 잘 사용하기만 하면 어렵지 않게 안정적이고 보안적으로 보장되는 서비스의 구현이 가능할 것입니다.
부가적인 설명을 드리자면 JDK 버전과 Java Enterprise 버전은 서로 범위가 다릅니다. Java Standard기능은 JDK 버전과 같지만 Java Enterprise 기능은 JDK 버전과는 별개로 WAS에서 지원하는 컴포넌트의 요소에 따라 그 버전을 달리 합니다. 앞서 대표적인 국내 프레임워크로 Spring 프레임워크를 사용하는 전자정부 프레임워크를 예로 들었지만 국내와는 달리 국외 통계를 본다면 Java Enterprise의 사용은 그리 생소하지 않습니다.
따라서 Java Enterprise 환경을 고려한다면 톰캣을 기반으로 한 TomEE를 활용하는 방법도 톰캣에 익숙한 개발자들에게는 도움이 될 수 있겠습니다.
',5);function k(T,A){const l=r("ExternalLinkIcon");return i(),s("div",null,[g,f,t("p",null,[e("톰캣의 특성상 쉽게 접할 수 있는 메뉴얼적인 지식보다는, 톰캣을 더 잘 사용하고 운영 할 수 있을만한 아이디어를 공유하고자 시작한 지식공유 활동입니다. 담고 있는 내용은 "),t("strong",null,[e("'"),t("a",c,[e("톰캣 알고 쓰기"),a(l)]),e("'")]),e(" 유튜브 강의 내용에 대한 정리입니다. 유튜브에 강의를 올리면 출퇴근 시간을 이용해 짬짬히 들을 수 있을 것 같은 생각이 들어 시작하였지만 "),y,e(" 동영상으로 모든 것을 다 표현할 수 없다는 점을 감안하여 다시 글로 정리합니다.")]),p,x,m,h,u,J,t("p",null,[e("Java 웹 어플리케이션을 실행하는 Application Server의 종류는 거의 30가지에 달하나 조사된 "),t("a",_,[e("JRebel의 통계"),a(l)]),e("에 따르면 여전히 과반 이상을 Tomcat이 점유하고 있다고 합니다. 여러가지 이유가 더 있겠지만 앞서 언급한 많은 레퍼런스와 무료라는 큰 특징으로 인해 수많은 사용자가 톰캣을 사용하고 있습니다.")]),S,t("p",null,[e("TomEE의 대표 홈페이지는 "),t("a",b,[e("tomee.apache.org"),a(l)]),e("이며 상세한 설명과 문서가 준비되어있기 때문에 사용하는데 큰 어려움이 없습니다. TomEE에서 지원하는 Java Enterprise 컴포넌트들은 다음과 같습니다.")]),t("p",null,[t("a",v,[e("https://tomee.apache.org/comparison.html"),a(l)])]),E])}const W=n(o,[["render",k],["__file","01-Introduction.html.vue"]]),L=JSON.parse(`{"path":"/05-Software/Tomcat/tomcat101/01-Introduction.html","title":"1. Tomcat 소개","lang":"ko-KR","frontmatter":{"description":"Tomcat","tag":["Tomcat","Java"],"head":[["meta",{"property":"og:url","content":"https://docmoa.github.io/05-Software/Tomcat/tomcat101/01-Introduction.html"}],["meta",{"property":"og:site_name","content":"docmoa"}],["meta",{"property":"og:title","content":"1. Tomcat 소개"}],["meta",{"property":"og:description","content":"Tomcat"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://marvel-b1-cdn.bc0a.com/f00000000156946/www.jrebel.com/sites/default/files/image/2020-01/7.%20what%20application%20server%20do%20you%20use%20on%20main%20application.png"}],["meta",{"property":"og:locale","content":"ko-KR"}],["meta",{"property":"og:updated_time","content":"2023-09-18T13:12:54.000Z"}],["meta",{"name":"twitter:card","content":"summary_large_image"}],["meta",{"name":"twitter:image:alt","content":"1. Tomcat 소개"}],["meta",{"property":"article:tag","content":"Tomcat"}],["meta",{"property":"article:tag","content":"Java"}],["meta",{"property":"article:modified_time","content":"2023-09-18T13:12:54.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"1. Tomcat 소개\\",\\"image\\":[\\"https://marvel-b1-cdn.bc0a.com/f00000000156946/www.jrebel.com/sites/default/files/image/2020-01/7.%20what%20application%20server%20do%20you%20use%20on%20main%20application.png\\",\\"https://raw.githubusercontent.com/Great-Stone/images/master/uPic/app-server-used-most-often-graph.jpg?token=ADUAZXKGEU5LVPPWSH3R4YK67EUKK\\",\\"https://raw.githubusercontent.com/Great-Stone/images/master/uPic/tomcat_component_do.jpg?token=ADUAZXMO7ZXOL64TU2WHS7267EULI\\"],\\"dateModified\\":\\"2023-09-18T13:12:54.000Z\\",\\"author\\":[]}"]]},"headers":[{"level":2,"title":"1.1 왜 톰캣을 쓰는가?","slug":"_1-1-왜-톰캣을-쓰는가","link":"#_1-1-왜-톰캣을-쓰는가","children":[]},{"level":2,"title":"1.2 톰캣 이력","slug":"_1-2-톰캣-이력","link":"#_1-2-톰캣-이력","children":[{"level":3,"title":"1.3 톰캣 구성 요소","slug":"_1-3-톰캣-구성-요소","link":"#_1-3-톰캣-구성-요소","children":[]}]},{"level":2,"title":"1.4 TomEE?","slug":"_1-4-tomee","link":"#_1-4-tomee","children":[]}],"git":{"createdTime":1640327880000,"updatedTime":1695042774000,"contributors":[{"name":"Administrator","email":"admin@example.com","commits":1},{"name":"Great-Stone","email":"hahohh@gmail.com","commits":1}]},"readingTime":{"minutes":1.32,"words":396},"filePathRelative":"05-Software/Tomcat/tomcat101/01-Introduction.md","localizedDate":"2021년 12월 24일","excerpt":"\\n본 내용은 톰캣을 좀더 잘 알고 잘 써보기 위한 제안이랄까요?
\\n톰캣의 특성상 쉽게 접할 수 있는 메뉴얼적인 지식보다는, 톰캣을 더 잘 사용하고 운영 할 수 있을만한 아이디어를 공유하고자 시작한 지식공유 활동입니다. 담고 있는 내용은 '톰캣 알고 쓰기' 유튜브 강의 내용에 대한 정리입니다. 유튜브에 강의를 올리면 출퇴근 시간을 이용해 짬짬히 들을 수 있을 것 같은 생각이 들어 시작하였지만 얼마나 출퇴근 시간에 이용하셨을지는 미지수이고 동영상으로 모든 것을 다 표현할 수 없다는 점을 감안하여 다시 글로 정리합니다.
실행이 완료되면 로그에 다음과 같은 메시지와 접속할 수 있는 링크가 나타납니다.
success [10:48:28] Build 6f9dd7 finished in 1179 ms! ( http://localhost:8000/ )
+
웹브라우저에서 실행시 표기되는 로그의 링크를 입력하면 공개된 웹화면과 동일한 환경을 확인할 수 있습니다.
typora
는 멀티 OS를 지원하는 마크다운 에디터/뷰어 입니다. VuePress의 플러그인과 일부 호환되지 않는 표기들이 있으나, 전역 검색이 가능하고 개인 노트를 활용하듯 관리할 수 있습니다.
3.typora
를 실행하고 옵션에서 [파일] > [열기] 를 클릭하여 앞서 받은 소스 디렉토리의 docs
디렉토리를 열어줍니다.
git clone https://github.com/docmoa/docs.git
+
VS Code
를 실행하고 [파일] > [열기] 를 클릭하여 앞서 받은 소스 디렉토리의 docs
디렉토리를 열어줍니다.Preview
버튼을 클릭하면 양쪽으로 비교하면서 글을 확인/편집 할 수 있습니다.문서가 인터넷상에 공개되는 목적은 접근성을 극대화 하기 위함 입니다. 또한 로컬환경에서 빠르게 문서를 검색하기 위해 해당 git repo를 clone 받거나 download 받아서 별도의 마크다운 툴과 연동하는 것도 가능합니다. VuePress
기반으로 구성되었기 때문에 이외의 방식은 문서 표기에 제약이 있을 수 있습니다.
docmoa의 공개된 페이지를 통해 문서를 읽을 수 있습니다.
"}');export{j as comp,q as data}; diff --git a/assets/01-Start.html-CZXt5onJ.js b/assets/01-Start.html-CZXt5onJ.js new file mode 100644 index 0000000000..5e803b634d --- /dev/null +++ b/assets/01-Start.html-CZXt5onJ.js @@ -0,0 +1,56 @@ +import{_ as r}from"./vuepress-DVMSUmm5.js";import{_ as h}from"./plugin-vue_export-helper-DlAUqK2U.js";import{r as c,o as u,c as m,b as a,d as n,a as e,w as t,e as s}from"./app-Bzk8Nrll.js";const g={},v=a("h1",{id:"문서작성-시작",tabindex:"-1"},[a("a",{class:"header-anchor",href:"#문서작성-시작"},[a("span",null,"문서작성 '시작'")])],-1),b=a("p",null,"docmoa에 문서 기여하기위한 가이드를 설명합니다.",-1),k=a("div",{class:"hint-container tip"},[a("p",{class:"hint-container-title"},"팁"),a("p",null,[n("다양한 방법으로 문서를 작성하고 기여할 수 있습니다."),a("br"),n(" 얽매이지 마세요.")])],-1),_=s(`git clone https://github.com/docmoa/docs.git
+
또는 github에서 fork하여 별도 관리 후 pull request 하여도 좋습니다.
clone 받은 구조는 VuePress의 구조를 갖고 있습니다. 문서의 기준이 되는 디렉토리는 docs
입니다.
디렉토리 구조
.
+├── .gitignore
+├── LICENSE.md
+├── README.md
+├── \`docs\`
+│ ├── .vuepress
+│ ├── 00-Howto
+│ ├── 01-Infra
+│ ├── 02-PrivatePlatform
+│ ├── 03-PublicCloud
+│ ├── 04-HashiCorp
+│ ├── 05-etc
+│ ├── 98-tag
+│ ├── 99-about
+│ └── README.md
+└── package.json
+
브라우저에서 보여지는 화면을 실시간으로 확인하기 위해 로컬환경에서 Vewpress를 실행합니다. Nodejs가 필요합니다.
`,7),f=a("div",{class:"language-bash","data-ext":"sh","data-title":"sh"},[a("pre",{"bash:no-line-numbers":"",class:"language-bash"},[a("code",null,[a("span",{class:"token comment"},"# 클론 받은 디렉토리 이동 후 npm install"),n(` +`),a("span",{class:"token builtin class-name"},"cd"),n(` docs +`),a("span",{class:"token function"},"npm"),n(),a("span",{class:"token function"},"install"),n(` + +`),a("span",{class:"token comment"},"# start VuePress writing"),n(` +`),a("span",{class:"token function"},"npm"),n(` run dev +`)])]),a("div",{class:"highlight-lines"},[a("br"),a("div",{class:"highlight-line"}," "),a("br"),a("br"),a("div",{class:"highlight-line"}," "),a("div",{class:"highlight-line"}," ")])],-1),E=a("div",{class:"language-bash","data-ext":"sh","data-title":"sh"},[a("pre",{"bash:no-line-numbers":"",class:"language-bash"},[a("code",null,[a("span",{class:"token comment"},"# 클론 받은 디렉토리 이동 후 npm install"),n(` +`),a("span",{class:"token builtin class-name"},"cd"),n(` docs +`),a("span",{class:"token function"},"yarn"),n(),a("span",{class:"token function"},"install"),n(` + +`),a("span",{class:"token comment"},"# start VuePress writing"),n(` +`),a("span",{class:"token function"},"yarn"),n(` vuepress dev +`)])]),a("div",{class:"highlight-lines"},[a("br"),a("div",{class:"highlight-line"}," "),a("br"),a("br"),a("div",{class:"highlight-line"}," "),a("div",{class:"highlight-line"}," ")])],-1),w=s(`실행이 완료되면 로그에 다음과 같은 메시지와 접속할 수 있는 링크가 나타납니다.
실행 후 접속 링크 출력
✔ Initializing and preparing data - done in 12.34s
+
+ vite v4.4.9 dev server running at:
+
+ ➜ Local: http://localhost:8080/
+ ➜ Network: http://192.168.0.9:8080/
+
웹브라우저에서 실행시 표기되는 로그의 링크를 입력하면 공개된 웹화면과 동일한 환경을 확인할 수 있습니다.
docs
디렉토리 내에 기존에 구성된 항목 내에 작성도 가능하고 새로운 카테고리를 생성할 수도 있습니다. 디렉토리와 파일은 생성된 순서와 관계없이 정렬되기 때문에 좌측 Sidebar
표기시 원하는 순서대로 표기되기를 원하는 경우 {숫자}-
을 파일명 앞에 붙여 의도한대로 표시되도록 구성가능합니다.
예를 들어 Howto
에 작성된 내용을 바탕으로 설명합니다.
문서 트리
00-Howto
+├── \`01-Overview.md\`
+├── \`02-Guide\`
+│ ├── 01-Start.md
+│ ├── 02-PullRequest.md
+│ └── 03-Fork.md
+├── \`03-Tips\`
+│ ├── CodeBlock.md
+│ ├── Link.md
+│ └── TipBox.md
+└── \`README.md\`
+
디렉토리 이름
디렉토리 이름에 공백이 있는 경우 다른 문서에서 참조 시 공백 문자인 %20
를 표기해야 하는 이슈가 발생할 수 있습니다.
[](https://docmoa.github.io/00-Howto/공백이%20있는%20디렉토리)
+
Linux
카테고리로 가정하고 01.Infra > Linux > TroubleShooting
이라는 디렉토리를 생성합니다.
문서 작성을 위한 SSH Too many authentication failures.md
파일을 생성합니다.
Title
이 메뉴에 표기됩니다.문서 내용에는 문서 기본 서문(Frontmatter)을 작성하기 위한 ---
로 구성된 블록이 최상단에 명시됩니다. 내용은 기존 마크다운 문서를 작성하는 것과 동일하게 구성합니다.
---
+
+---
+
+# SSH Too many authentication failures (제목인 Title은 h1 스타일로)
+
+너무많은 인증 실패로 인한 SSH 접속이 안되는 메시지를 간혹 보게되는 경우가 있다.
+<생략>
+
docmoa에 문서 기여하기위한 가이드를 설명합니다.
\\n팁
\\n다양한 방법으로 문서를 작성하고 기여할 수 있습니다.
\\n얽매이지 마세요.
문서는 모두 git으로 관리되며 공개되어있습니다. 문서 기여를 위한 방식은 별도 안내로 구분하여 설명합니다.
","autoDesc":true}`);export{N as comp,I as data}; diff --git a/assets/01-cicd.html-OsUvvkt4.js b/assets/01-cicd.html-OsUvvkt4.js new file mode 100644 index 0000000000..f60409ab1f --- /dev/null +++ b/assets/01-cicd.html-OsUvvkt4.js @@ -0,0 +1 @@ +import{_ as i}from"./plugin-vue_export-helper-DlAUqK2U.js";import{r as t,o,c as l,a as c,b as e,e as a}from"./app-Bzk8Nrll.js";const r={},s=e("h1",{id:"_1-ci-cd",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#_1-ci-cd"},[e("span",null,"1. CI/CD")])],-1),d=e("h2",{id:"_1-1-ci-cd-concept-definitions",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#_1-1-ci-cd-concept-definitions"},[e("span",null,"1.1 CI/CD Concept Definitions")])],-1),p=e("ul",null,[e("li",null,"Continuous integration"),e("li",null,"Continuous delivery"),e("li",null,"Continuous deployment"),e("li",null,"Source control management (SCM)")],-1),m=a('이번에 연재할 스터디는 AWS EKS Workshop Study (=AEWS)이다. AWS에서 공식적으로 제공되는 다양한 HOL 기반의 Workshop과 가시다님의 팀에서 2차 가공한 컨텐츠를 기반으로 진행한다.
필자는 기본적인 스터디내용을 이번 시리즈에 연재할 예정이며, 추가적으로 HashiCorp의 Consul, Vault 등을 샘플로 배포하며 연동하는 내용을 조금씩 다뤄볼 예정이다.
',6),u={href:"https://www.eksworkshop.com/",target:"_blank",rel:"noopener noreferrer"},k={href:"https://github.com/aws-samples/eks-workshop-v2",target:"_blank",rel:"noopener noreferrer"},d={href:"https://aws.amazon.com/ko/blogs/containers/introducing-the-amazon-eks-workshop/",target:"_blank",rel:"noopener noreferrer"},m={href:"https://www.youtube.com/@ContainersfromtheCouch",target:"_blank",rel:"noopener noreferrer"},b={href:"https://www.youtube.com/@ContainersfromtheCouch/streams",target:"_blank",rel:"noopener noreferrer"},v={href:"https://catalog.us-east-1.prod.workshops.aws/workshops/9c0aa9ab-90a9-44a6-abe1-8dff360ae428/ko-KR",target:"_blank",rel:"noopener noreferrer"},g={href:"https://awskrug.github.io/eks-workshop/",target:"_blank",rel:"noopener noreferrer"},h={href:"https://awskocaptain.gitbook.io/aws-builders-eks/",target:"_blank",rel:"noopener noreferrer"},y={href:"https://tf-eks-workshop.workshop.aws/",target:"_blank",rel:"noopener noreferrer"},f={href:"https://www.youtube.com/live/TXa-y-Uwh2w?feature=share",target:"_blank",rel:"noopener noreferrer"},S={href:"https://aws.github.io/aws-eks-best-practices/",target:"_blank",rel:"noopener noreferrer"},E={href:"https://catalog.us-east-1.prod.workshops.aws/workshops/b67b6665-f7a2-427f-affb-caccd087d50d/en-US",target:"_blank",rel:"noopener noreferrer"},w={href:"https://catalog.us-east-1.prod.workshops.aws/workshops/c15012ac-d05d-46b1-8a4a-205e7c9d93c9/en-US",target:"_blank",rel:"noopener noreferrer"},A={href:"https://catalog.workshops.aws/observability/en-US",target:"_blank",rel:"noopener noreferrer"},_={href:"https://catalog.us-east-1.prod.workshops.aws/workshops/a1101fcc-c7cf-4dd5-98c4-f599a65056d5/en-US",target:"_blank",rel:"noopener noreferrer"},C={href:"https://catalog.workshops.aws/general-immersionday/ko-KR",target:"_blank",rel:"noopener noreferrer"},P={href:"https://whchoi98.gitbook.io/aws-iam/",target:"_blank",rel:"noopener noreferrer"},T=t('참고 : AWS EKS 관련 핸즈온 워크샵을 해볼 수 있는 다양한 링크 모음이다.
여담이지만, HashiCorp 솔루션에 대한 다양한 HOL Workshop 실습도 사용자들이 많이 만들고 기여할 수 있도록 플랫폼을 오픈하면 좋을 것 같다.
Amazon Elastic Kubernetes Service는 자체 Kubernetes 컨트롤 플레인 또는 노드를 설치, 운영 및 유지 관리할 필요 없이 Kubernetes 실행에 사용할 수 있는 관리형 서비스이다.
EKS를 사용하는 다양한 이유가 있겠지만 대표적으로 여러 AWS 서비스와 통합할 수 있다는 장점이 크다.
실습환경은 외부에서 접근 가능한 Bastion 역할을 하는 EC2와 퍼블릭 서브넷 2개에 워커노드 두 대를 구성한다.
참고 : 실습환경 변경 챕터에서 노드를 3대로 증설예정
간단하게 VPC, Security Group, EC2 등을 구성하는 CF 코드를 통해 사전 환경을 구성한다.
aws cloudformation deploy --template-file myeks-1week.yaml \\
+ --stack-name myeks --parameter-overrides KeyName=hw-key SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32 --region ap-northeast-2
+
+Waiting for changeset to be created..
+Waiting for stack create/update to complete
+Successfully created/updated stack - myeks
+
+# Public IP 확인
+aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[*].OutputValue' --output text
+13.124.14.182
+
+# ec2 에 SSH 접속
+ssh -i ~/.ssh/id_rsa ec2-user@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)
+
필자의 경우에는 AWS IAM 계정 정책에 대한 제약사항이 있어, aws configure
명령등으로 access_key
, secret_key
설정을 하지않고 EC2 인스턴스에 admin 권한을 부여하여 사용하였다.
eks-admin
이라고하는 admin 권한의 역할을 생성 후 부여# EKS 배포할 VPC 정보 확인
+export VPCID=$(aws ec2 describe-vpcs --filters "Name=tag:Name,Values=$CLUSTER_NAME-VPC" | jq -r .Vpcs[].VpcId)
+echo "export VPCID=$VPCID" >> /etc/profile
+echo VPCID
+
+## 퍼블릭 서브넷 ID 확인
+export PubSubnet1=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-PublicSubnet1" --query "Subnets[0].[SubnetId]" --output text)
+export PubSubnet2=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-PublicSubnet2" --query "Subnets[0].[SubnetId]" --output text)
+echo "export PubSubnet1=$PubSubnet1" >> /etc/profile
+echo "export PubSubnet2=$PubSubnet2" >> /etc/profile
+echo $PubSubnet1
+echo $PubSubnet2
+
+# EKS 클러스터 배포
+eksctl create cluster --name $CLUSTER_NAME --region=$AWS_DEFAULT_REGION --nodegroup-name=$CLUSTER_NAME-nodegroup --node-type=t3.medium --node-volume-size=30 --vpc-public-subnets "$PubSubnet1,$PubSubnet2" --version 1.24 --ssh-access --external-dns-access --verbose 4
+
EKS 클러스터를 명령형으로 배포하지 않고 YAML로 작성하여 언언적으로 배포하는 것도 가능하다. 다음은 앞서 실행한 eksctl create cluster
명령의 --dry-run
옵션을 통해 추출한 명세이다.
apiVersion: eksctl.io/v1alpha5
+cloudWatch:
+ clusterLogging: {}
+iam:
+ vpcResourceControllerPolicy: true
+ withOIDC: false
+kind: ClusterConfig
+kubernetesNetworkConfig:
+ ipFamily: IPv4
+managedNodeGroups:
+- amiFamily: AmazonLinux2
+ desiredCapacity: 2
+ disableIMDSv1: false
+ disablePodIMDS: false
+ iam:
+ withAddonPolicies:
+ albIngress: false
+ appMesh: false
+ appMeshPreview: false
+ autoScaler: false
+ awsLoadBalancerController: false
+ certManager: false
+ cloudWatch: false
+ ebs: false
+ efs: false
+ externalDNS: true
+ fsx: false
+ imageBuilder: false
+ xRay: false
+ instanceSelector: {}
+ instanceType: t3.medium
+ labels:
+ alpha.eksctl.io/cluster-name: myeks
+ alpha.eksctl.io/nodegroup-name: myeks-nodegroup
+ maxSize: 2
+ minSize: 2
+ name: myeks-nodegroup
+ privateNetworking: false
+ releaseVersion: ""
+ securityGroups:
+ withLocal: null
+ withShared: null
+ ssh:
+ allow: true
+ publicKeyPath: ~/.ssh/id_rsa.pub
+ tags:
+ alpha.eksctl.io/nodegroup-name: myeks-nodegroup
+ alpha.eksctl.io/nodegroup-type: managed
+ volumeIOPS: 3000
+ volumeSize: 30
+ volumeThroughput: 125
+ volumeType: gp3
+metadata:
+ name: myeks
+ region: ap-northeast-2
+ version: "1.24"
+privateCluster:
+ enabled: false
+ skipEndpointCreation: false
+vpc:
+ autoAllocateIPv6: false
+ cidr: 192.168.0.0/16
+ clusterEndpoints:
+ privateAccess: false
+ publicAccess: true
+ id: vpc-0521fc003559b2f2c
+ manageSharedNodeSecurityGroupRules: true
+ nat:
+ gateway: Disable
+ subnets:
+ public:
+ ap-northeast-2a:
+ az: ap-northeast-2a
+ cidr: 192.168.1.0/24
+ id: subnet-0fdff27653277aaf0
+ ap-northeast-2c:
+ az: ap-northeast-2c
+ cidr: 192.168.2.0/24
+ id: subnet-084a8752d4c7ddf6c
+
정상적으로 클러스터가 구성된 것을 확인할 수 있다.
# eks클러스터 확인
+eksctl get cluster
+NAME REGION EKSCTL CREATED
+myeks ap-northeast-2 True
+
+# 노드확인
+kubectl get node -v=6
+I0423 22:10:48.050969 2339 loader.go:374] Config loaded from file: /root/.kube/config
+
+I0423 22:10:48.880262 2339 round_trippers.go:553] GET https://6E205513BA73EEBC3CA693BADEEC5294.gr7.ap-northeast-2.eks.amazonaws.com/api/v1/nodes?limit=500 200 OK in 819 milliseconds
+NAME STATUS ROLES AGE VERSION
+ip-192-168-1-139.ap-northeast-2.compute.internal Ready <none> 61m v1.24.11-eks-a59e1f0
+ip-192-168-2-225.ap-northeast-2.compute.internal Ready <none> 61m v1.24.11-eks-a59e1f0
+
+# 파드확인
+k get pods -A
+NAMESPACE NAME READY STATUS RESTARTS AGE
+kube-system aws-node-2bpxr 1/1 Running 0 62m
+kube-system aws-node-s7p5b 1/1 Running 0 62m
+kube-system coredns-dc4979556-knkkh 1/1 Running 0 68m
+kube-system coredns-dc4979556-m789b 1/1 Running 0 68m
+kube-system kube-proxy-lkp8f 1/1 Running 0 62m
+kube-system kube-proxy-z6hbk 1/1 Running 0 62m
+
참고 :
eksctl
명령 예제
# eksctl help
+eksctl
+eksctl create
+eksctl create cluster --help
+eksctl create nodegroup --help
+
앞선 과정을 통해 실습을 위한 클러스터 구성이 완성되었다. 필자는 향후 샘플 애플리케이션으로 Vault, Consul 등을 배포할 예정이다. 때문에, 최소 3대 이상의 노드가 필요하여 기본 실습 노드를 3대로 구성한다.
EKS는 nodegraup
개수의 최소/최대 개수를 선언적으로 관리할 수 있다. 다음은 노드 개수를 변경/확인 하는 방법이다.
# eks 노드 그룹 정보 확인
+eksctl get nodegroup --cluster $CLUSTER_NAME --name $CLUSTER_NAME-nodegroup
+CLUSTER NODEGROUP STATUS CREATED MIN SIZE MAX SIZE DESIRED CAPACITY INSTANCE TYPE IMAGE ID ASG NAME TYPE
+myeks myeks-nodegroup UPDATING 2023-04-23T12:07:57Z 2 2 2 t3.medium AL2_x86_64 eks-myeks-nodegroup-fcc3d701-b90a-9c83-7907-fca8459770b9 managed
+
+# 노드 2개 → 3개 증가
+eksctl scale nodegroup --cluster $CLUSTER_NAME --name $CLUSTER_NAME-nodegroup --nodes 3 --nodes-min 3 --nodes-max 6
+
+# 노드 그룹 변경 확인
+eksctl get nodegroup --cluster myeks --region ap-northeast-2 --name myeks-nodegroup
+CLUSTER NODEGROUP STATUS CREATED MIN SIZE MAX SIZE DESIRED CAPACITY INSTANCE TYPE IMAGE ID ASG NAME TYPE
+myeks myeks-nodegroup UPDATING 2023-04-23T12:07:57Z 3 6 3 t3.medium AL2_x86_64 eks-myeks-nodegroup-fcc3d701-b90a-9c83-7907-fca8459770b9 managed
+
+# 노드 확인
+kubectl get nodes -o wide
+
+NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
+ip-192-168-1-139.ap-northeast-2.compute.internal Ready <none> 150m v1.24.11-eks-a59e1f0 192.168.1.139 43.201.51.34 Amazon Linux 2 5.10.176-157.645.amzn2.x86_64 containerd://1.6.19
+ip-192-168-1-76.ap-northeast-2.compute.internal Ready <none> 53s v1.24.11-eks-a59e1f0 192.168.1.76 13.124.158.208 Amazon Linux 2 5.10.176-157.645.amzn2.x86_64 containerd://1.6.19
+ip-192-168-2-225.ap-northeast-2.compute.internal Ready <none> 150m v1.24.11-eks-a59e1f0 192.168.2.225 52.79.236.227 Amazon Linux 2 5.10.176-157.645.amzn2.x86_64 containerd://1.6.19
+
+kubectl get nodes -l eks.amazonaws.com/nodegroup=$CLUSTER_NAME-nodegroup
+NAME STATUS ROLES AGE VERSION
+ip-192-168-1-139.ap-northeast-2.compute.internal Ready <none> 150m v1.24.11-eks-a59e1f0
+ip-192-168-1-76.ap-northeast-2.compute.internal Ready <none> 74s v1.24.11-eks-a59e1f0
+ip-192-168-2-225.ap-northeast-2.compute.internal Ready <none> 150m v1.24.11-eks-a59e1f0
+
필자는 본 글을 작성하던 시기에 고객사 환경에 ArgoCD + Helm을 기반으로 Consul Cluster 구성 테스트 요청이 있어 해당 클러스터를 활용해 보았다.
참고 : PKOS 2기에 사용한 ArgoCD 배포 가이드를 참고하여 배포한다.
OutOfSync
에러를 출력.# 네임스페이스 생성
+kubectl create ns argocd
+
+# argocd-helm 설치
+cd
+helm repo add argo https://argoproj.github.io/argo-helm
+helm repo update
+helm install argocd argo/argo-cd --set server.service.type=LoadBalancer --namespace argocd --version 5.19.14
+
+# 확인
+helm list -n argocd
+kubectl get pod,pvc,svc,deploy,sts -n argocd
+kubectl get-all -n argocd
+
+kubectl get crd | grep argoproj
+applications.argoproj.io 2023-03-19T11:39:26Z
+applicationsets.argoproj.io 2023-03-19T11:39:26Z
+appprojects.argoproj.io 2023-03-19T11:39:26Z
+
+# admin 계정의 암호 확인
+ARGOPW=$(kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d)
+echo $ARGOPW
+mf8bOtNEq7iHMqq1
+
+# 웹 접속 로그인 (admin) CLB의 hostname으로 접속
+k get svc -n argocd argocd-server -o jsonpath='{.status.loadBalancer.ingress[].hostname}'
+
사전에 로컬에서 다운로드 받은 Consul Helm Chart 파일에 새로 재정의 할 values 파일을 GitHub 저장소에 업로드 한다.
참고 : 다음은 실제 작성된 Values 파일이다. 각 항목에 대한 상세한 설명은 향후 Consul Deploy on K8s 가이드를 작성해서 업로드 예정이다.
client:
+ grpc: true
+connectInject:
+ consulNamespaces:
+ mirroringK8S: true
+ enabled: true
+controller:
+ enabled: true
+global:
+ acls:
+ manageSystemACLs: true
+ enableConsulNamespaces: true
+ enterpriseLicense:
+ secretKey: key
+ secretName: license
+ gossipEncryption:
+ autoGenerate: true
+ image: hashicorp/consul-enterprise:1.13.7-ent
+ imageEnvoy: envoyproxy/envoy:v1.22.5
+ imageK8S: hashicorp/consul-k8s-control-plane:0.49.5
+ metrics:
+ enabled: false
+ tls:
+ enableAutoEncrypt: true
+ enabled: true
+ httpsOnly: false
+ verify: false
+ingressGateways:
+ defaults:
+ replicas: 1
+ service:
+ type: LoadBalancer
+ enabled: true
+ gateways:
+ - name: ingress-gateway
+meshGateway:
+ enabled: false
+ replicas: 1
+ service:
+ enabled: true
+ nodePort: 32000
+ type: NodePort
+server:
+ replicas: 3
+terminatingGateways:
+ defaults:
+ replicas: 1
+ enabled: false
+ui:
+ enabled: true
+ service:
+ port:
+ http: 80
+ https: 443
+ type: LoadBalance
+
ArgoCD CLI를 통해 필자의 GitHub을 연동한다
argocd login <argocd 주소> --username admin --password $ARGOPW
+...
+'admin:login' logged in successfully
+
+argocd repo add https://github.com/<깃헙 계정명>/<레파지토리명> --username <깃헙 계정명> --password <깃헙 계정 암호>
+
+argocd repo list
+TYPE NAME REPO INSECURE OCI LFS CREDS STATUS MESSAGE PROJECT
+git https://github.com/chosam2/gitops.git false false false true Successful
+
+# 기본적으로 아르고시디가 설치된 쿠버네티스 클러스터는 타깃 클러스터로 등록됨
+argocd cluster list
+SERVER NAME VERSION STATUS MESSAGE PROJECT
+https://kubernetes.default.svc in-cluster 1.24+ Successful
+
앞서 생성한 GitHub 저장소에 업로드한 consul helm values 파일을 통해 배포하기 위해 Application
CRD를 생성 및 배포한다.
consul-helm-argo-application.yaml
apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name: consul-helm
+ namespace: argocd
+ finalizers:
+ - resources-finalizer.argocd.argoproj.io
+spec:
+ destination:
+ namespace: consul
+ server: https://kubernetes.default.svc
+ project: default
+ source:
+ repoURL: https://github.com/chosam2/gitops.git
+ path: argocd
+ targetRevision: main
+ helm:
+ valueFiles:
+ - override-values.yaml
+ syncPolicy:
+ syncOptions:
+ - CreateNamespace=true
+
k apply -f consul-helm-argo-application.yaml
+application.argoproj.io/consul-helm created
+
최초 배포 시 OutOfSync
상태로 배포되었지만, 동기화 버튼을 클릭하여 강제로 동기화해준 뒤 정상적으로 배포된 것을 확인할 수 있다.
다만, 최초 OutOfSync
상태로 배포되는 부분에 대해서는 Application YAML 작성 시 옵션을 통해 해결이 가능하지만, 실제 운영시 영향도 체크가 필요해 보인다. 이 부분은 다음 블로깅시에 조금 더 테스트 및 확인해볼 예정이다.
1주차 스터디는 EKS에 대한 전반적인 컨셉과 기본적으로 클러스터를 구성하고 Consul Cluster를 간단하게 배포해보았다. 다음주에는 네트워킹을 주제로 찾아올 예정이다.
`,23);function V(U,j){const s=p("ExternalLinkIcon");return o(),i("div",null,[r,n("ul",null,[n("li",null,[a("[AWS] EKS Workshop - "),n("a",u,[a("링크"),e(s)]),a(" / Github - "),n("a",k,[a("링크"),e(s)]),a(" / Blog - "),n("a",d,[a("링크"),e(s)]),a(" / Youtube - "),n("a",m,[a("링크"),e(s)]),a(),n("a",b,[a("Streams"),e(s)])]),n("li",null,[a("[한글] EKS 웹 앱 구축 - "),n("a",v,[a("링크"),e(s)]),a(" / (Old) Amazon EKS 워크샵 - "),n("a",g,[a("링크"),e(s)])]),n("li",null,[a("[한글/whchoi98] EKS Hands On LAB - "),n("a",h,[a("링크"),e(s)])]),n("li",null,[a("[AWS] EKS Terraform Workshop - "),n("a",y,[a("링크"),e(s)]),a(" / Youtube - "),n("a",f,[a("링크"),e(s)])]),n("li",null,[a("[AWS] EKS Best Practices Guides - "),n("a",S,[a("링크"),e(s)])]),n("li",null,[a("[AWS workshop studio] Running batch workloads on Amazon EKS with AWS Batch - "),n("a",E,[a("링크"),e(s)])]),n("li",null,[a("[AWS workshop studio] Manage your EKS cluster in Full-stack with CDK - "),n("a",w,[a("링크"),e(s)])]),n("li",null,[a("[AWS workshop studio] One Observability Workshop - "),n("a",A,[a("링크"),e(s)])]),n("li",null,[a("[AWS workshop studio] Web Application Hosts on EKS Workshop - "),n("a",_,[a("링크"),e(s)])]),n("li",null,[a("[한글/AWS workshop studio] AWS General Immersion Day - "),n("a",C,[a("링크"),e(s)])]),n("li",null,[a("[한글/whchoi98] AWS IAM Hands On LAB - "),n("a",P,[a("링크"),e(s)])])]),T,n("p",null,[a("다만 단점(?)이라고 할 수 있는 부분은 지원 버전이 보통 4개의 마이너 버전 지원(현재 1.22~1.26), 평균 3개월마다 새 버전 제공, 각 버전은 12개월 정도 지원한다는 것이다. "),n("a",R,[a("링크"),e(s)])]),x,K,N,n("blockquote",null,[n("p",null,[a("참고 : EKS 업데이트 캘린더 : "),n("a",I,[a("https://endoflife.date/amazon-eks"),e(s)])])]),z,O,L,W,n("blockquote",null,[n("p",null,[a("eksctl : EKS 클러스터 구축 및 관리를 하기 위한 오프소스 명령줄 도구 - "),n("a",D,[a("링크"),e(s)])])]),q,n("blockquote",null,[n("p",null,[a("필자는 개인 GitHub에 "),n("a",M,[a("개인 퍼블릭 저장소"),e(s)]),a("를 만들어서 실습을 진행하였다.")])]),G])}const B=l(c,[["render",V],["__file","01-eks-deploy.html.vue"]]),Y=JSON.parse('{"path":"/02-PrivatePlatform/Kubernetes/06-EKS/01-eks-deploy.html","title":"AEWS 1주차 - Amzaon EKS 설치 및 기본 사용","lang":"ko-KR","frontmatter":{"description":"AWS에서 공식적으로 제공되는 다양한 HOL 기반의 Workshop과 가시다님의 팀에서 2차 가공한 컨텐츠를 기반으로 진행한다.","tag":["Kubernetes","EKS","PKOS"],"head":[["meta",{"property":"og:url","content":"https://docmoa.github.io/02-PrivatePlatform/Kubernetes/06-EKS/01-eks-deploy.html"}],["meta",{"property":"og:site_name","content":"docmoa"}],["meta",{"property":"og:title","content":"AEWS 1주차 - Amzaon EKS 설치 및 기본 사용"}],["meta",{"property":"og:description","content":"AWS에서 공식적으로 제공되는 다양한 HOL 기반의 Workshop과 가시다님의 팀에서 2차 가공한 컨텐츠를 기반으로 진행한다."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://raw.githubusercontent.com/hyungwook0221/img/main/uPic/751VTo.jpg"}],["meta",{"property":"og:locale","content":"ko-KR"}],["meta",{"property":"og:updated_time","content":"2023-09-18T13:12:54.000Z"}],["meta",{"name":"twitter:card","content":"summary_large_image"}],["meta",{"name":"twitter:image:alt","content":"AEWS 1주차 - Amzaon EKS 설치 및 기본 사용"}],["meta",{"property":"article:tag","content":"Kubernetes"}],["meta",{"property":"article:tag","content":"EKS"}],["meta",{"property":"article:tag","content":"PKOS"}],["meta",{"property":"article:modified_time","content":"2023-09-18T13:12:54.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"AEWS 1주차 - Amzaon EKS 설치 및 기본 사용\\",\\"image\\":[\\"https://raw.githubusercontent.com/hyungwook0221/img/main/uPic/751VTo.jpg\\",\\"https://static.us-east-1.prod.workshops.aws/public/e7ab9b91-502d-4ada-84e2-5c8b92d8f791/static/images/10-intro/eks_architecture.svg\\",\\"https://eksctl.io/img/eksctl.png\\",\\"https://raw.githubusercontent.com/hyungwook0221/img/main/uPic/6ulJkf.jpg\\",\\"https://raw.githubusercontent.com/hyungwook0221/img/main/uPic/eObYCj.jpg\\",\\"https://raw.githubusercontent.com/hyungwook0221/img/main/uPic/helm_argo.png\\",\\"https://raw.githubusercontent.com/hyungwook0221/img/main/uPic/T5kZFT.jpg\\",\\"https://raw.githubusercontent.com/hyungwook0221/img/main/uPic/aY0au9.jpg\\",\\"https://raw.githubusercontent.com/hyungwook0221/img/main/uPic/argo_%E1%84%80%E1%85%AE%E1%84%8C%E1%85%A9.png\\",\\"https://raw.githubusercontent.com/hyungwook0221/img/main/uPic/argo_%E1%84%80%E1%85%AE%E1%84%8C%E1%85%A92.png\\",\\"https://raw.githubusercontent.com/hyungwook0221/img/main/uPic/ejor4D.jpg\\"],\\"dateModified\\":\\"2023-09-18T13:12:54.000Z\\",\\"author\\":[]}"]]},"headers":[{"level":2,"title":"AWS Workshop - EKS 관련","slug":"aws-workshop-eks-관련","link":"#aws-workshop-eks-관련","children":[]},{"level":2,"title":"Amazon EKS 소개","slug":"amazon-eks-소개","link":"#amazon-eks-소개","children":[]},{"level":2,"title":"Amazon EKS 클러스터 구성 방안","slug":"amazon-eks-클러스터-구성-방안","link":"#amazon-eks-클러스터-구성-방안","children":[]},{"level":2,"title":"실습환경 구성","slug":"실습환경-구성","link":"#실습환경-구성","children":[]},{"level":2,"title":"실습환경 변경","slug":"실습환경-변경","link":"#실습환경-변경","children":[]},{"level":2,"title":"샘플 애플리케이션 배포","slug":"샘플-애플리케이션-배포","link":"#샘플-애플리케이션-배포","children":[{"level":3,"title":"ArgoCD 배포","slug":"argocd-배포","link":"#argocd-배포","children":[]},{"level":3,"title":"GitHub 저장소 준비","slug":"github-저장소-준비","link":"#github-저장소-준비","children":[]},{"level":3,"title":"ArgoCD + GitHub 연동","slug":"argocd-github-연동","link":"#argocd-github-연동","children":[]},{"level":3,"title":"ArgoCD Application 생성 및 배포","slug":"argocd-application-생성-및-배포","link":"#argocd-application-생성-및-배포","children":[]}]},{"level":2,"title":"마무리","slug":"마무리","link":"#마무리","children":[]}],"git":{"createdTime":1695042774000,"updatedTime":1695042774000,"contributors":[{"name":"Great-Stone","email":"hahohh@gmail.com","commits":1}]},"readingTime":{"minutes":5.19,"words":1556},"filePathRelative":"02-PrivatePlatform/Kubernetes/06-EKS/01-eks-deploy.md","localizedDate":"2023년 9월 18일","excerpt":"\\n이번에 연재할 스터디는 AWS EKS Workshop Study (=AEWS)이다. AWS에서 공식적으로 제공되는 다양한 HOL 기반의 Workshop과 가시다님의 팀에서 2차 가공한 컨텐츠를 기반으로 진행한다.
\\n\\n필자는 기본적인 스터디내용을 이번 시리즈에 연재할 예정이며, 추가적으로 HashiCorp의 Consul, Vault 등을 샘플로 배포하며 연동하는 내용을 조금씩 다뤄볼 예정이다.
"}');export{B as comp,Y as data}; diff --git a/assets/01-infrastructure_maturity.html-CSXVrsyb.js b/assets/01-infrastructure_maturity.html-CSXVrsyb.js new file mode 100644 index 0000000000..be6b87b8c1 --- /dev/null +++ b/assets/01-infrastructure_maturity.html-CSXVrsyb.js @@ -0,0 +1 @@ +import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{o as i,c as a,b as e,e as r}from"./app-Bzk8Nrll.js";const n={},o=e("h1",{id:"인프라의-변화와-적응",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#인프라의-변화와-적응"},[e("span",null,"인프라의 변화와 적응")])],-1),l=e("iframe",{width:"560",height:"315",src:"https://www.youtube.com/embed/HB3LMVLNi_Q",frameborder:"0",allow:"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture",allowfullscreen:""},null,-1),s=r('이번에는 인프라의 변화와 적응이라는 제목으로 인프라의 성숙도와 관련한 이야기를 나누고자 합니다.
HashiCorp의 테라폼을 이야기하면서 함께 이야기되는 것은 언제나 Infrastructure as Code라는 IaC 입니다. 즉, 인프라는 코드로 설명되고로 테라폼은 이를 지원하는 멋진 툴입니다.
IaC는 많은 의미를 갖고 있지만, 이 이야기를 하기에 앞서 결국 이것도 코드 이기 때문에 최근의 DevOps 같은 맥락의 이야기를 할수 밖에 없을 것 같습니다.
사람이 과거에서 오늘날 까지 진화를 했든 우리의 인프라도 각 시기마다 적절한 운영 성숙도를 갖추었습니다.
첫단계에서는 매뉴얼을 사용합니다. 인프라에 대한 모든 정보와 구성 방법, 변경 방법, 기존 아키텍쳐에 대한 내용은 문서로 관리되었습니다. 여전히 엑셀 시트를 가지고 인프라 아이피와 아이디, 서비스가 어떤게 올라가 있는지 관리하는 곳도 많이 있지요. 변경에 대한 모든 사항은 문서로 남겨야 하고, 그렇지 못한다면 기억에 의존해야 합니다.
나름의 노하우를 담아 스크립팅 언어를 사용하여 작업합니다. 새로운 서버를 설정하거나 기능을 수행하기위한 작업으로 상당히 간단하고 편리합니다. 이단계에서는 비슷한 인프라나 애플리케이션 런타임이 일반적이고 조직간에 많은 의사 소통이 필요하지 않기 때문에 상당히 유용합니다. 일단 노가다를 많이 줄여줍니다. 특징은 다음과 같습니다.
이제 이미 설정된 머신을 가상화를 통해 미리 저장해두고 사용합니다. 미리 필요한 패키지나 솔루션을 설치해두고 바로 사용 가능합니다. 미리 이미지들을 만드는 작업을 수행하기 때문에 가상머신을 사용하면 자동화를 향상시킬 수 있고 공동작업도 향상됩니다.
다음은 이제 우리가 기본적으로 컨트롤 가능한 클라우드 리소스와 함께하는 시대입니다. 인프라를 더이상 소유하지 않고 상품화된 인프라, 데이터 센터를 사용하기 위해서 우리는 자동화를 요구받기 시작합니다. API를 통해 더 많은 기술 계층을 가상화 하고 더 많은 소프트웨어와 자동화 기술로 지금의 데이터 센터의 컴퓨터를 포함한 인프라를 대신하는 환경을 제공합니다.
이제 더 나아가 머신이 아닌 OS를 가상화 하기 시작합니다. 데이터 센터의 컴퓨터를 사용하는 대신 컴퓨터를 자원 풀의 하나로 여기고, 어디인지는 몰라도 애플리케이션을 적당한 리소스가 있는 그 어딘가에 배포합니다. 리소스 활용율을 높이고 이전 보다 더더더 자동화합니다. Agile이 요구되고 DevOps를 해야한다고 합니다.
더 많은 자동화를 하면 이론적으로 우리는 더 적은 일을 해야합니다. 하지만 전반적으로 자동화의 단계를 살펴보면 이론적으로 가상화를 늘리고 컨테이너화를 하는 것은 우리의 삶을 더 좋게 만들어야 하는데, 정말 그런가요? 때때로 자동화는 정말 무수히 많은 반복과 검증 작업이 필요하고 이전보다 더 많은 시간과 노력을 요구합니다. 자동화나 DevOps에대한 장난스럽지만 마음을 후비는 글들도 넘쳐나죠. 자동화를 했음에도 불구하고 다시 새로운 플랜을 실행해야 하는 상황이나 여전히 우리가 인프라를 대하는 마음가짐이 하드웨어로 바라보고 있는 시각 처럼 말입니다.
아마도 유발하라리의 소설 사피엔스를 재미있게 보신 분들은 호모사피엔스 뿐만아니라 다른 종도 함께 살았었다는 흥미로운 가정을 하는 이야기를 보셨을 것입니다. (다른종을 멸종시켰다고..) 그리고 소설에서의 이야기 처럼 앞서 다룬 자동화외에도 수십, 혹은 수백가지의 자동화를 위한 내 한몸 편하고자 하는 몸부림이 현 시점에도 병렬로 존재합니다. 퍼블릭과 프라이빗 클라우드 환경이 도입되고 있고 VM과 베어메탈 환경은 여전히 존재합니다. 더불어 더작은 엣지, IoT 인프라도 관리의 대상이 되고 있습니다.
인프라의 자동화를 이야기 함에 있어 빼뜨릴 수 없는것이 Infrastructure as Code, IoC 입니다. 코드로서의 인프라스트럭쳐, IaC가 취하는 전략이 무엇일까요? 다년간의 경험을 가진 팀이 보유한 시스템 환경을 코드로 바꾸면 무엇이 달라질까요?
그럼 몇가지 상황을 제시해보겠습니다.
코드로 인프라를 관리한다는 것은 자유롭게 변경하고 환경을 이해하고 반복적으로 동일한 상황으로 만들 수 있다는 점입니다. 그리고 그 명세를 별도의 문서로 정리할 필요 없이 명확하게 인프라가 정의되어 남아있습니다.
우선 잘 만들어지는, 좋은 인프라 자동화를 이야기 하기 앞서 그 조건을 만드는 좋은 코드의 특징을 찾아보고 몇가지 공통된 항목을 뽑아보고 다음과 같이 정리해보았습니다.
이외에도 몇가지 더 있겠지만 이런 좋은 코드의 특징과 IaC가 어떤 관계가 있을까요? 인프라도 마치 좋은 코드처럼 관리가 가능하다는 것입니다. 앞서 자동화를 위해 문서화 했어야 했고, 종속성을 분석하여 관리하고 인프라 자원의 변경이 있을때마다 변경하고 그 결과물, 혹은 결과물을 만들어내는 도구를 관리하고 다시 사용할 수 있게 만드는 것. 그것은 좋은 코드와 좋은 인프라 자동화 방식이 같은 맥락으로 이어질 수 있도록 만들어주는 IaC의 특징입니다. 하지만 '좋은' 이라는 수식이 붙는 인프라의 자동화가 그저 쉽게만 되지는 않을 수 있습니다. 물론 인프라를 위한 좋은 코드는 연습이 필요합니다.
인프라 프로비저닝의 최우선 목표는 재현 가능한 인프라를 코드로 제공하는 것입니다. DevOps팀이 CI/CD 워크플로우 내에서 익숙한 도구를 사용하여 리소스를 계획하고 프로비저닝을 할 수 있는 방법을 제공하는 것입니다. Terraform은 DevOps 팀이나 클라우드 팀에서 구성한 아키텍쳐를 코드로 템플릿화 하고 기본적인 리소스와 세분화된 프로비저닝을 처리할 수 있습니다. 이런 구성은 주요 인프라 관리 도구와 통합되고 모니터링, APM 시스템, 보안 도구, 네트워크 등을 포함하여 다른 많은 ISV 공급자의 서비스로 확장 할 수 있습니다. 정의된 템플릿은 자동화된 방식으로 필요에 따라 프로비저닝 하고, 이를 통해 Terraform은 퍼블릭과 프라이빗 클라우드에 리소스를 프로비저닝 하는 공통 워크플로우를 생성합니다.
그리고 엔터프라이즈 환경을 위한 지원은 무엇이 있을까요? Terraform Enterprise는 오픈 소스의 코드 프로비저닝으로 인프라 스트럭처에서 협업, 거버넌스 및 셀프 서비스 워크 플로우를 제공합니다. Terraform Enterprise는 팀이 협력하여 인프라를 구축 할 수 있도록 워크스페이스, 모듈과 모듈 레지스트리, 거버넌스를 위한 구성을 제공합니다.
모듈의 활용은 기존에 티켓 방식으로 수행하던 인프라 프로비저닝 워크플로우에서 인프라를 재사용 가능한 모듈에 코드로 패키지하여 개발자가 셀프 서비스 방식으로 신속하게 프로비저닝 할 수 있는 환경을 만들어 줍니다.
그리고 프로비저닝과 관련한 정책 및 코드 로깅을 통해 조직은 전체 배포를 보호, 관리 및 감사로그를 확인 할 수 있습니다.
끊임없이 성장하고 확장되는 인프라 환경에서 Infrastructure as Code라는 아이디어가 탄생했습니다. IaC에 기반한 가치는 관리되는 프로비저닝으로 기존 레거시 환경과 퍼블릭/프라이빗 클라우드에 대한 비용을 최적화하고 기존 대비 빠른 속도와 위험 감소라는 측면에서 지금의 우리가 맞이하고 있는 현 시대에 적합한 모델이라고 볼 수 있습니다. DevOps의 핵심 구성요소기기도 하나 좋은 코드가 좋은 인프라를 만드는 것처럼 품질관릴와 체계적인 거버넌스를 구성하기 위한 노력이 반드시 필요한 영역입니다.
',49),c=[o,l,s];function m(p,u){return i(),a("div",null,c)}const d=t(n,[["render",m],["__file","01-infrastructure_maturity.html.vue"]]),f=JSON.parse('{"path":"/04-HashiCorp/03-Terraform/01-Information/01-infrastructure_maturity.html","title":"인프라의 변화와 적응","lang":"ko-KR","frontmatter":{"description":"인프라의 변화와 적응","tag":["terraform","IaC"],"head":[["meta",{"property":"og:url","content":"https://docmoa.github.io/04-HashiCorp/03-Terraform/01-Information/01-infrastructure_maturity.html"}],["meta",{"property":"og:site_name","content":"docmoa"}],["meta",{"property":"og:title","content":"인프라의 변화와 적응"}],["meta",{"property":"og:description","content":"인프라의 변화와 적응"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://raw.githubusercontent.com/Great-Stone/images/master/uPic/image-20200707110714298.png"}],["meta",{"property":"og:locale","content":"ko-KR"}],["meta",{"property":"og:updated_time","content":"2023-09-18T13:12:54.000Z"}],["meta",{"name":"twitter:card","content":"summary_large_image"}],["meta",{"name":"twitter:image:alt","content":"인프라의 변화와 적응"}],["meta",{"property":"article:tag","content":"terraform"}],["meta",{"property":"article:tag","content":"IaC"}],["meta",{"property":"article:modified_time","content":"2023-09-18T13:12:54.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"인프라의 변화와 적응\\",\\"image\\":[\\"https://raw.githubusercontent.com/Great-Stone/images/master/uPic/image-20200707110714298.png\\",\\"https://raw.githubusercontent.com/Great-Stone/images/master/uPic/image-20200701184003515.png\\",\\"https://raw.githubusercontent.com/Great-Stone/images/master/uPic/SBSEFUIa2AnSd8xKoXo-8J1HplKEGMY9UuFkqABcnfNkxjuSTg8yVk-C9WNFVSMUYaUdud_xjLoqPBp07hvCEYztIqveyuhoUWLZpXm694Ptp8y_mwMQbYIXq-NhVVZ_9ansWOi3_t0.png\\",\\"https://raw.githubusercontent.com/Great-Stone/images/master/uPic/image-20200706221237156.png\\",\\"https://raw.githubusercontent.com/Great-Stone/images/master/uPic/ppkX01eIvJ1Qn1dYKIMr0ckfCWyeYQOiNXFGaMZJkoj1oC1XidCnwhbiUbeeKpbKkLasVSr0UvVQa49u4OAOXgEJv0u3CxbnOu7Pg61tpSJXwXG5aKck411ixrEF9SwnQDDb_oWVkbI.png\\",\\"https://raw.githubusercontent.com/Great-Stone/images/master/uPic/image-20200706222116243.png\\",\\"https://raw.githubusercontent.com/Great-Stone/images/master/uPic/image-20200707180213752.png\\",\\"https://raw.githubusercontent.com/Great-Stone/images/master/uPic/image-20200707085213583.png\\",\\"https://raw.githubusercontent.com/Great-Stone/images/master/uPic/image-20200707091154907.png\\"],\\"dateModified\\":\\"2023-09-18T13:12:54.000Z\\",\\"author\\":[]}"]]},"headers":[{"level":2,"title":"자동화와 그 변화에 대해","slug":"자동화와-그-변화에-대해","link":"#자동화와-그-변화에-대해","children":[{"level":3,"title":"1. Manual Everything","slug":"_1-manual-everything","link":"#_1-manual-everything","children":[]},{"level":3,"title":"2. Basic Automation","slug":"_2-basic-automation","link":"#_2-basic-automation","children":[]},{"level":3,"title":"3. Machine Virtualization","slug":"_3-machine-virtualization","link":"#_3-machine-virtualization","children":[]},{"level":3,"title":"4. Commoditization of Infrastructure","slug":"_4-commoditization-of-infrastructure","link":"#_4-commoditization-of-infrastructure","children":[]},{"level":3,"title":"5. Datacenter as Computer","slug":"_5-datacenter-as-computer","link":"#_5-datacenter-as-computer","children":[]},{"level":3,"title":"자동화 딜레마","slug":"자동화-딜레마","link":"#자동화-딜레마","children":[]}]},{"level":2,"title":"Infrastructure as Code","slug":"infrastructure-as-code","link":"#infrastructure-as-code","children":[{"level":3,"title":"좋은 코드","slug":"좋은-코드","link":"#좋은-코드","children":[]},{"level":3,"title":"IaC starting with Terraform","slug":"iac-starting-with-terraform","link":"#iac-starting-with-terraform","children":[]},{"level":3,"title":"Terraform Enterprise Workflow","slug":"terraform-enterprise-workflow","link":"#terraform-enterprise-workflow","children":[]}]},{"level":2,"title":"Conclusion","slug":"conclusion","link":"#conclusion","children":[]}],"git":{"createdTime":1640262000000,"updatedTime":1695042774000,"contributors":[{"name":"Administrator","email":"admin@example.com","commits":1},{"name":"Great-Stone","email":"hahohh@gmail.com","commits":1}]},"readingTime":{"minutes":0.69,"words":208},"filePathRelative":"04-HashiCorp/03-Terraform/01-Information/01-infrastructure_maturity.md","localizedDate":"2021년 12월 23일","excerpt":"\\n\\n이번에는 인프라의 변화와 적응이라는 제목으로 인프라의 성숙도와 관련한 이야기를 나누고자 합니다.
\\n"}');export{d as comp,f as data}; diff --git a/assets/01-kops-on-aws.html-ROAC0cUk.js b/assets/01-kops-on-aws.html-ROAC0cUk.js new file mode 100644 index 0000000000..c817d8dbf9 --- /dev/null +++ b/assets/01-kops-on-aws.html-ROAC0cUk.js @@ -0,0 +1,209 @@ +import{_ as o}from"./plugin-vue_export-helper-DlAUqK2U.js";import{r as l,o as i,c as p,b as n,d as s,a as t,e as a}from"./app-Bzk8Nrll.js";const c={},r=a('💡 본 글은 PKOS(Production Kubernetes Online Study) 2기 스터디의 일부로 작성된 내용입니다.
실제 Production Kubernetes 환경에서 활용 가능한 다양한 정보를 전달하기 위한 시리즈로 작성 예정입니다.
본 스터디는 AWS 환경에서 Kops(Kubernetes Operations)를 활용한 실습으로 진행할 예정입니다.
📌 참고 : 필자는 개인적인 이유로 Route 53 계정과, kOps 클러스터 운영 계정을 나눠서 진행합니다.
하나의 계정에서 실습을 진행할 경우에는 사전 환경구성이 다를 수 있는 점 참고 부탁드립니다.
구입 시 등록했던 이메일 계정으로 발송된 verify 메일 링크를 클릭하여 활성화 합니다.
일정시간이 지나면 정상적으로 도메인이 활성화 되된 것을 확인할 수 있습니다.
# 자신의 도메인에 NS 타입 조회
+# dig ns <구입한 자신의 도메인> +short
+dig ns hyungwook.link +short
+ns-939.awsdns-53.net.
+ns-1575.awsdns-04.co.uk.
+ns-233.awsdns-29.com.
+ns-1466.awsdns-55.org.
+
필자는 서두에서 언급한 것 처럼 Route 53 구매한 계정과, kOps 클러스터를 생성할 계정이 다르므로 다음과 같은 과정을 추가적으로 수행하였습니다.
pkos.hyungwook.link
레코드를 생성이제 실습에서 사용할 도메인 준비가 완료되었으므로, Kops 클러스터 생성을 위한 준비 단계로 넘어갑니다.
# IAM User 자격 구성 : 실습 편리를 위해 administrator 권한을 가진 IAM User 의 자격 증명 입력
+aws configure
+
# k8s 설정 파일이 저장될 버킷 생성
+## aws s3 mb s3://버킷<유일한 이름> --region <S3 배포될 AWS 리전>
+aws s3 mb s3://버킷<유일한 이름> --region $REGION
+aws s3 ls
+
+## 예시)
+aws s3 mb s3://hyungwook-k8s-s3 --region ap-northeast-2
+
# 변수설정
+export AWS_PAGER=""
+export REGION=ap-northeast-2
+export KOPS_CLUSTER_NAME=pkos.hyungwook.link
+export KOPS_STATE_STORE=s3://hyungwook-k8s-s3
+echo 'export AWS_PAGER=""' >>~/.bashrc
+echo 'export REGION=ap-northeast-2' >>~/.bashrc
+echo 'export KOPS_CLUSTER_NAME=pkos.hyungwook.link' >>~/.bashrc
+echo 'export KOPS_STATE_STORE=s3://hyungwook-k8s-s3' >>~/.bashrc
+
+# kops 설정 파일 생성(s3) 및 k8s 클러스터 배포 : 6분 정도 소요
+## CNI는 aws vpc cni 사용, 마스터 노드 1대(t3.medium), 워커 노드 2대(t3.medium), 파드 사용 네트워크 대역 지정(172.30.0.0/16)
+## --container-runtime containerd --kubernetes-version 1.24.0 ~ 1.25.6
+kops create cluster --zones="$REGION"a,"$REGION"c --networking amazonvpc --cloud aws \\
+--master-size t3.medium --node-size t3.medium --node-count=2 --network-cidr 172.30.0.0/16 \\
+--ssh-public-key ~/.ssh/id_rsa.pub --name=$KOPS_CLUSTER_NAME --kubernetes-version "1.24.10" --dry-run -o yaml > mykops.yaml
+
+kops create cluster --zones="$REGION"a,"$REGION"c --networking amazonvpc --cloud aws \\
+--master-size t3.medium --node-size t3.medium --node-count=2 --network-cidr 172.30.0.0/16 \\
+--ssh-public-key ~/.ssh/id_rsa.pub --name=$KOPS_CLUSTER_NAME --kubernetes-version "1.24.10" -y
+
A 레코드값이 자동으로 추가된 모습을 확인할 수 있습니다. 하지만 실제 api 서버와 내부 controller의 IP 주소가 등록되지 않았기 때문에, 실제 클러스터가 정상적으로 구성된 이후에는 자동으로 A 레코드가 업데이트 됩니다.
aws route53
aws route53 list-resource-record-sets --hosted-zone-id "\${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'A'].Name" | jq
+
+[
+ "api.pkos.hyungwook.link.",
+ "api.internal.pkos.hyungwook.link.",
+ "kops-controller.internal.pkos.hyungwook.link."
+]
+
이때, kops validate
명령으로 확인하면 아직까지 api.pkos.hyungwook.link
가 relov 되지 않는 것을 확인할 수 있습니다.
kops validate cluster --wait 10m
+Validating cluster pkos.hyungwook.link
+
+W0305 22:38:08.780600 4256 validate_cluster.go:184] (will retry): unexpected error during validation: unable to resolve Kubernetes cluster API URL dns: lookup api.pkos.hyungwook.link: no such host
+W0305 22:38:18.788067 4256 validate_cluster.go:184] (will retry): unexpected error during validation: unable to resolve Kubernetes cluster API URL dns: lookup api.pkos.hyungwook.link: no such host
+
어느정도 시간이 지난 후 정상적으로 A 레코드 값이 변경된 것을 확인할 수 있습니다.
# A 레코드 및 값 반복조회
+while true; do aws route53 list-resource-record-sets --hosted-zone-id "\${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'A']" | jq ; date ; echo ; sleep 1; done
+[
+ {
+ "Name": "api.pkos.hyungwook.link.",
+ "Type": "A",
+ "TTL": 60,
+ "ResourceRecords": [
+ {
+ "Value": "43.201.33.161"
+ }
+ ]
+ },
+ {
+ "Name": "api.internal.pkos.hyungwook.link.",
+ "Type": "A",
+ "TTL": 60,
+ "ResourceRecords": [
+ {
+ "Value": "172.30.37.41"
+ }
+ ]
+ },
+ {
+ "Name": "kops-controller.internal.pkos.hyungwook.link.",
+ "Type": "A",
+ "TTL": 60,
+ "ResourceRecords": [
+ {
+ "Value": "172.30.37.41"
+ }
+ ]
+ }
+]
+2023년 3월 5일 일요일 22시 41분 46초 KST
+
이제 정상적으로 A 레코드가 등록된 것을 확인할 수 있으며 설치가 자동으로 진행됩니다.
kops validate cluster
명령(생성확인)실제 kops 클러스터가 정상적으로 배포된 것을 확인할 수 있습니다.
kops validate cluster
+Validating cluster pkos.hyungwook.link
+
+INSTANCE GROUPS
+NAME ROLE MACHINETYPE MIN MAX SUBNETS
+master-ap-northeast-2a Master t3.medium 1 1 ap-northeast-2a
+nodes-ap-northeast-2a Node t3.medium 1 1 ap-northeast-2a
+nodes-ap-northeast-2c Node t3.medium 1 1 ap-northeast-2c
+
+NODE STATUS
+NAME ROLE READY
+i-089062ff9f50789ee node True
+i-096a645be0dd932b6 node True
+i-0dce8997b4633b806 master True
+
+Your cluster pkos.hyungwook.link is ready
+
📌 참고 : Kops 클러스터
kubeconfig
파일 업데이트 명령
# 권한이 없을 경우
+kubectl get nodes -o wide
+error: You must be logged in to the server (Unauthorized)
+
+# kubeconfig 업데이트
+kops export kubeconfig --name pkos.hyungwook.link --admin
+
# 수퍼마리오 디플로이먼트 배포
+curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/1/mario.yaml
+kubectl apply -f mario.yaml
+cat mario.yaml | yh
+deployment.apps/mario created
+service/mario created
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: mario
+ labels:
+ app: mario
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: mario
+ template:
+ metadata:
+ labels:
+ app: mario
+ spec:
+ containers:
+ - name: mario
+ image: pengbai/docker-supermario
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: mario
+spec:
+ selector:
+ app: mario
+ ports:
+ - port: 80
+ protocol: TCP
+ targetPort: 8080
+ type: LoadBalancer
+
# 배포 확인 : CLB 배포 확인 >> 5분 이상 소요
+kubectl get deploy,svc,ep mario
+watch kubectl get svc mario
+
+# Watch 명령 실행 후 <pending>
+Every 2.0s: kubectl get svc mario hyungwook-Q9W5QX7FGY: Sat Mar 11 21:50:41 2023
+NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
+mario LoadBalancer 100.67.138.178 <pending> 80:30624/TCP 92s
+
+# External-IP 할당
+kubectl get svc mario
+NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
+mario LoadBalancer 100.67.138.178 a643cc3e6e2c54ed8989c95d0481f48c-113657418.ap-northeast-2.elb.amazonaws.com 80:30624/TCP 3m7s
+
# 마리오 게임 접속 : CLB 주소로 웹 접속
+kubectl get svc mario -o jsonpath="{.status.loadBalancer.ingress[0].hostname}" | awk '{ print "Maria URL = http://"$1 }'
+
+# 결과 값
+Maria URL = http://a643cc3e6e2c54ed8989c95d0481f48c-113657418.ap-northeast-2.elb.amazonaws.com
+
External DNS은 K8s Service / Ingress 생성 시 도메인을 설정하면 자동으로 AWS Route53의 A 레코드(TXT 레코드)에 자동 생성/삭제를 제공합니다.
`,39),h={href:"https://edgehog.blog/a-self-hosted-external-dns-resolver-for-kubernetes-111a27d6fc2c",target:"_blank",rel:"noopener noreferrer"},_=a(`# 정책 생성 -> 마스터/워커노드에 정책 연결
+curl -s -O https://s3.ap-northeast-2.amazonaws.com/cloudformation.cloudneta.net/AKOS/externaldns/externaldns-aws-r53-policy.json
+aws iam create-policy --policy-name AllowExternalDNSUpdates --policy-document file://externaldns-aws-r53-policy.json
+
+# ACCOUNT_ID 변수 지정
+export ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)
+
+# EC2 instance profiles 에 IAM Policy 추가(attach)
+aws iam attach-role-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AllowExternalDNSUpdates --role-name masters.$KOPS_CLUSTER_NAME
+aws iam attach-role-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AllowExternalDNSUpdates --role-name nodes.$KOPS_CLUSTER_NAME
+
+# 설치
+kops edit cluster
+--------------------------
+spec:
+ certManager: # 없어도됨!
+ enabled: true # 없어도됨!
+ externalDns:
+ provider: external-dns
+--------------------------
+
+# 업데이트 적용
+kops update cluster --yes && echo && sleep 3 && kops rolling-update cluster
+
+# externalDns 컨트롤러 파드 확인
+kubectl get pod -n kube-system -l k8s-app=external-dns
+NAME READY STATUS RESTARTS AGE
+external-dns-5bc8fcf8-7vznp 1/1 Running 0 14s
+
# CLB에 ExternanDNS 로 도메인 연결
+kubectl annotate service mario "external-dns.alpha.kubernetes.io/hostname=mario.$KOPS_CLUSTER_NAME"
+
# external-dns 등록로그 확인
+kubectl logs -n kube-system -l k8s-app=external-dns
+
+time="2023-03-11T14:54:51Z" level=info msg="Applying provider record filter for domains: [pkos.hyungwook.link. .pkos.hyungwook.link.]"
+time="2023-03-11T14:54:51Z" level=info msg="All records are already up to date"
+...(생략)
+
+# 확인
+dig +short mario.$KOPS_CLUSTER_NAME
+
+# 웹 접속 주소 확인 및 접속
+echo -e "Maria Game URL = http://mario.$KOPS_CLUSTER_NAME"
+
+# 도메인 체크
+echo -e "My Domain Checker = https://www.whatsmydns.net/#A/mario.$KOPS_CLUSTER_NAME"
+
kubectl delete deploy,svc mario
+
kops delete cluster --yes
+
본 편에서는 Kops 클러스터를 구성방안 및 External DNS를 연동한 외부 서비스 노출에 대한 방법을 살펴보았습니다.
다음편에서는 네트워크 및 스토리지에 대한 활용방안을 살펴보겠습니다.
`,11);function f(q,x){const e=l("ExternalLinkIcon");return i(),p("div",null,[r,n("p",null,[s("AWS의 DNS 웹 서비스인 "),n("a",u,[s("Route 53"),t(e)]),s("을 통해 도메인을 구입합니다."),d,s(" 필자는 "),m,s(" 도메인을 구입하였으며, 초기 구입 후 "),k,s(" 인 화면을 확인할 수 있습니다,")]),v,n("blockquote",null,[n("p",null,[s("📌 참고 : "),n("a",b,[s("How to manage Route53 hosted zones in a multi-account environment"),t(e)])])]),g,n("ul",null,[n("li",null,[s("이미지 참고 "),n("a",h,[s("링크"),t(e)])])]),_,n("ul",null,[n("li",null,[s("사이트를 통한 확인 - "),n("a",y,[s("참고"),t(e)])])]),w])}const E=o(c,[["render",f],["__file","01-kops-on-aws.html.vue"]]),P=JSON.parse('{"path":"/02-PrivatePlatform/Kubernetes/05-Kops/01-kops-on-aws.html","title":"[PKOS] 1편 - AWS Kops 설치 및 기본 사용","lang":"ko-KR","frontmatter":{"description":"AWS Kops 설치 및 기본 사용","tag":["Kubernetes","Kops","EKS","PKOS"],"head":[["meta",{"property":"og:url","content":"https://docmoa.github.io/02-PrivatePlatform/Kubernetes/05-Kops/01-kops-on-aws.html"}],["meta",{"property":"og:site_name","content":"docmoa"}],["meta",{"property":"og:title","content":"[PKOS] 1편 - AWS Kops 설치 및 기본 사용"}],["meta",{"property":"og:description","content":"AWS Kops 설치 및 기본 사용"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://raw.githubusercontent.com/hyungwook0221/img/main/uPic/1_route53.png"}],["meta",{"property":"og:locale","content":"ko-KR"}],["meta",{"property":"og:updated_time","content":"2023-09-18T13:12:54.000Z"}],["meta",{"name":"twitter:card","content":"summary_large_image"}],["meta",{"name":"twitter:image:alt","content":"[PKOS] 1편 - AWS Kops 설치 및 기본 사용"}],["meta",{"property":"article:tag","content":"Kubernetes"}],["meta",{"property":"article:tag","content":"Kops"}],["meta",{"property":"article:tag","content":"EKS"}],["meta",{"property":"article:tag","content":"PKOS"}],["meta",{"property":"article:modified_time","content":"2023-09-18T13:12:54.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"[PKOS] 1편 - AWS Kops 설치 및 기본 사용\\",\\"image\\":[\\"https://raw.githubusercontent.com/hyungwook0221/img/main/uPic/1_route53.png\\",\\"https://raw.githubusercontent.com/hyungwook0221/img/main/uPic/2_route53.png\\",\\"https://raw.githubusercontent.com/hyungwook0221/img/main/uPic/3_route53.png\\",\\"https://raw.githubusercontent.com/hyungwook0221/img/main/uPic/4_route53_서브도메인등록.png\\",\\"https://raw.githubusercontent.com/hyungwook0221/img/main/uPic/5_route53_호스팅영영_등록중.png\\",\\"https://raw.githubusercontent.com/hyungwook0221/img/main/uPic/6_route53_호스팅영영_등록완료.png\\",\\"https://raw.githubusercontent.com/hyungwook0221/img/main/uPic/9_CLA_마리오게임_접속.png\\",\\"https://miro.medium.com/v2/resize:fit:1400/0*HoU4pgcDE10AVTAu.png\\",\\"https://raw.githubusercontent.com/hyungwook0221/img/main/uPic/ukvLhO.jpg\\",\\"https://raw.githubusercontent.com/hyungwook0221/img/main/uPic/jGLPD7.jpg\\"],\\"dateModified\\":\\"2023-09-18T13:12:54.000Z\\",\\"author\\":[]}"]]},"headers":[{"level":2,"title":"1. 실습환경 사전준비","slug":"_1-실습환경-사전준비","link":"#_1-실습환경-사전준비","children":[{"level":3,"title":"1) Route 53 도메인 구매","slug":"_1-route-53-도메인-구매","link":"#_1-route-53-도메인-구매","children":[]},{"level":3,"title":"2) Route 53 등록","slug":"_2-route-53-등록","link":"#_2-route-53-등록","children":[]}]},{"level":2,"title":"2. Kops 클러스터 배포 전 사전준비","slug":"_2-kops-클러스터-배포-전-사전준비","link":"#_2-kops-클러스터-배포-전-사전준비","children":[{"level":3,"title":"1) AWS Credentials 설정","slug":"_1-aws-credentials-설정","link":"#_1-aws-credentials-설정","children":[]},{"level":3,"title":"2) S3 버킷 생성","slug":"_2-s3-버킷-생성","link":"#_2-s3-버킷-생성","children":[]}]},{"level":2,"title":"3. 클러스터 배포","slug":"_3-클러스터-배포","link":"#_3-클러스터-배포","children":[{"level":3,"title":"1) kOps 클러스터 배포 시 Route 53 A 레코드 화면(등록중)","slug":"_1-kops-클러스터-배포-시-route-53-a-레코드-화면-등록중","link":"#_1-kops-클러스터-배포-시-route-53-a-레코드-화면-등록중","children":[]},{"level":3,"title":"2) kOps 클러스터 배포 시 Route 53 A 레코드 화면(등록완료 )","slug":"_2-kops-클러스터-배포-시-route-53-a-레코드-화면-등록완료","link":"#_2-kops-클러스터-배포-시-route-53-a-레코드-화면-등록완료","children":[]},{"level":3,"title":"3) kops validate cluster 명령(생성확인)","slug":"_3-kops-validate-cluster-명령-생성확인","link":"#_3-kops-validate-cluster-명령-생성확인","children":[]}]},{"level":2,"title":"4. 샘플 애플리케이션 배포","slug":"_4-샘플-애플리케이션-배포","link":"#_4-샘플-애플리케이션-배포","children":[{"level":3,"title":"1) Service / Pod with CLB : Mario 게임","slug":"_1-service-pod-with-clb-mario-게임","link":"#_1-service-pod-with-clb-mario-게임","children":[]}]},{"level":2,"title":"5. (추가) External DNS","slug":"_5-추가-external-dns","link":"#_5-추가-external-dns","children":[{"level":3,"title":"1) External DNS - Addon 설치","slug":"_1-external-dns-addon-설치","link":"#_1-external-dns-addon-설치","children":[]},{"level":3,"title":"2) Mario 서비스에 도메인 연결","slug":"_2-mario-서비스에-도메인-연결","link":"#_2-mario-서비스에-도메인-연결","children":[]}]},{"level":2,"title":"6. 마무리","slug":"_6-마무리","link":"#_6-마무리","children":[{"level":3,"title":"1) 리소스 삭제","slug":"_1-리소스-삭제","link":"#_1-리소스-삭제","children":[]}]}],"git":{"createdTime":1695042774000,"updatedTime":1695042774000,"contributors":[{"name":"Great-Stone","email":"hahohh@gmail.com","commits":1}]},"readingTime":{"minutes":3.52,"words":1055},"filePathRelative":"02-PrivatePlatform/Kubernetes/05-Kops/01-kops-on-aws.md","localizedDate":"2023년 9월 18일","excerpt":"\\n\\n\\n💡 본 글은 PKOS(Production Kubernetes Online Study) 2기 스터디의 일부로 작성된 내용입니다.
\\n
\\n실제 Production Kubernetes 환경에서 활용 가능한 다양한 정보를 전달하기 위한 시리즈로 작성 예정입니다.
본 스터디는 AWS 환경에서 Kops(Kubernetes Operations)를 활용한 실습으로 진행할 예정입니다.
\\n\\n"}');export{E as comp,P as data}; diff --git a/assets/01-terraform-intro.html-B1MhIda9.js b/assets/01-terraform-intro.html-B1MhIda9.js new file mode 100644 index 0000000000..b20f4263e4 --- /dev/null +++ b/assets/01-terraform-intro.html-B1MhIda9.js @@ -0,0 +1,27 @@ +import{_ as c}from"./plugin-vue_export-helper-DlAUqK2U.js";import{r as l,o as p,c as u,a as r,w as e,b as t,d as s,e as o}from"./app-Bzk8Nrll.js";const m="/assets/GUI01-BXuH5Jcw.png",h="/assets/GUI02-D7PXN0N9.png",g="/assets/GUI03-DrpUzYVO.png",v="/assets/GUI04-BArdWfmu.png",f="/assets/GUI05-D_nSi561.png",k="/assets/GUI06-CRJWTx6F.png",_="/assets/infra_tools-B1A7wyiT.png",b="/assets/cloud-provisioning-tools-Bt5prm77.png",N={},C=o('📌 참고 : 필자는 개인적인 이유로 Route 53 계정과, kOps 클러스터 운영 계정을 나눠서 진행합니다.
\\n
\\n하나의 계정에서 실습을 진행할 경우에는 사전 환경구성이 다를 수 있는 점 참고 부탁드립니다.
새로운 NCP의 인스턴스를 프로비저닝 할 수있는 몇 가지 다른 방법을 살펴 보겠습니다. 시작하기 전에 다음을 포함한 몇 가지 기본 정보를 수집해야합니다 (더 많은 옵션이 있습니다).
ncloud server createServerInstances \\
+ --serverImageProductCode SPSW0LINUX000046 \\
+ --serverProductCode SPSVRSTAND000003 \\
+ --serverName ncloud-mktest
+
파라미터 명 | 필수 여부 | 타입 | 제약사항 |
---|---|---|---|
serverImageProductCode | Conditional | String | Min:1, Max:20 |
serverProductCode | No | String | Min:1, Max:20 |
memberServerImageNo | Conditional | String | |
serverName | No | String | Min:3, Max:30 |
serverDescription | No | String | Min:1, Max:1000 |
loginKeyName | No | String | Min:3, Max:30 |
isProtectServerTermination | No | Boolean | |
serverCreateCount | No | Integer | Min:1, Max:20 |
serverCreateStartNo | No | Integer | |
internetLineTypeCode | No | String | Min:1, Max:5 |
feeSystemTypeCode | No | String | Min:1, Max:5 |
zoneNo | No | String | |
accessControlGroupConfigurationNoList | No | List | Min:0, Max:5 |
raidTypeName | Conditional | String | |
userData | No | String | Min:1, Max:21847 |
initScriptNo | No | String | |
instanceTagList.tagKey | No | String | |
instanceTagList.tagValue | No | String | |
isVaccineInstall | No | Boolean | |
blockDevicePartitionList.N.mountPoint | No | String | "/" (root) 경로로 시작하는 마운트 포인트를 입력합니다. 첫 번째 마운트 포인트는 반드시 "/" (root) 파티션이어야 합니다. "/" (root) 하위 명칭은 소문자와 숫자만 허용되며, 소문자로 시작해야합니다. OS 종류에 따라서 /root, /bin, /dev 등의 특정 키워드는 사용 불가능 할 수 있습니다. |
blockDevicePartitionList.N.partitionSize | No | String | Min : 50 GiB |
nCloud CLI는 자동화할 수 있는 스크립트 방식을 제공합니다. 하지만 이 작업을 실행하기 전에 예측할 수 있나요?
resource "ncloud_server" "server" {
+ name = "tf-test-vm1"
+ server_image_product_code = "SPSW0LINUX000032"
+ server_product_code = "SPSVRSTAND000004"
+
+ tag_list {
+ tag_key = "samplekey1"
+ tag_value = "samplevalue1"
+ }
+
+ tag_list {
+ tag_key = "samplekey2"
+ tag_value = "samplevalue2"
+ }
+}
+
resource "ncloud_server" "server" {
+ name = "tf-test-vm1"
+ server_image_product_code = "SPSW0LINUX000032"
+ server_product_code = "SPSVRSTAND000004"
+}
+
IaC (Infrastructure as Code)는 컴퓨터에서 읽을 수있는 정의 파일을 사용하여 클라우드 인프라를 관리하고 프로비저닝하는 프로세스입니다.
실행 가능한 '문서'라고 생각하시면 됩니다.
JSON:
"name": "{ "Fn::Join" : [ "-", [ PilotServerName, vm ] ] }",
+
Terraform:
name = "${var.PilotServerName}-vm"
+
Terraform 코드 (HCL)는 배우기 쉽고 읽기 쉽습니다. 또한 동등한 JSON 구성보다 50-70 % 더 간결합니다.
새로운 NCP의 인스턴스를 프로비저닝 할 수있는 몇 가지 다른 방법을 살펴 보겠습니다. 시작하기 전에 다음을 포함한 몇 가지 기본 정보를 수집해야합니다 (더 많은 옵션이 있습니다).
\\nFork
문서는 github상에서 관리됩니다. 우선 문서를 추가구성하고 수정할 수 있도록 원본 github repo를 Fork
합니다.
https://github.com/docmoa/docs
로 이동합니다.Fork
를 클릭하고 나의 github Org를 선택합니다.Fetch
or Pull
Fork의 원본 Repo에 변경에 대해 작업중인 Repo에 변경사항을 적용해야 하는 필요성이 있습니다. 여러 편집자가 동일한 시점에 동일 문서를 편집하게 되면 편집에 충돌이 발생할 수 있습니다.
',5),G=t("img",{src:"https://upload.wikimedia.org/wikipedia/commons/9/97/Paragraph-based_prototype_–_rough_visualization_of_the_functionality.png",alt:"Conflict",loading:"lazy"},null,-1),U=t("br",null,null,-1),O={href:"https://en.wikipedia.org/wiki/Edit_conflict",target:"_blank",rel:"noopener noreferrer"},T=t("p",null,"충돌을 사전에 최대한 방지하기 위해서 편집 전에 원본의 문서를 가져오고 병합하는 과정이 필요합니다.",-1),D=t("p",null,"CLI 컨트롤을 위해서는 앞서 git 유틸 설치가 필요합니다.",-1),H=t("div",{class:"language-bash line-numbers-mode","data-ext":"sh","data-title":"sh"},[t("pre",{class:"language-bash"},[t("code",null,[t("span",{class:"token comment"},"# 1. Fork 받은 본인 소유의 Repo를 Clone 받습니다."),e(` +`),t("span",{class:"token function"},"git"),e(` clone https://github.com/docmoa/docs + +`),t("span",{class:"token comment"},"# 2. 해당 소스 디렉토리로 이동하여 remote 를 확인합니다."),e(` +`),t("span",{class:"token builtin class-name"},"cd"),e(` docs +`),t("span",{class:"token function"},"git"),e(" remote "),t("span",{class:"token parameter variable"},"-v"),e(` +origin https://github.com/myorg/docs.git `),t("span",{class:"token punctuation"},"("),e("fetch"),t("span",{class:"token punctuation"},")"),e(` +origin https://github.com/myorg/docs.git `),t("span",{class:"token punctuation"},"("),e("push"),t("span",{class:"token punctuation"},")"),e(` + +`),t("span",{class:"token comment"},"# 3. 문서 원본 Repo와의 병합을 위해 `upstream` repo remote를 추가합니다."),e(` +`),t("span",{class:"token function"},"git"),e(" remote "),t("span",{class:"token function"},"add"),e(` upstream https://github.com/docmoa/docs + +`),t("span",{class:"token comment"},"# 4-1. pull 을 수행하거나"),e(` +`),t("span",{class:"token function"},"git"),e(` pull upstream main + +`),t("span",{class:"token comment"},"# 4-2. fetch & merge 를 수행합니다."),e(` +`),t("span",{class:"token function"},"git"),e(` fetch upstream +`),t("span",{class:"token function"},"git"),e(` merge upstream/main +`)])]),t("div",{class:"line-numbers","aria-hidden":"true"},[t("div",{class:"line-number"}),t("div",{class:"line-number"}),t("div",{class:"line-number"}),t("div",{class:"line-number"}),t("div",{class:"line-number"}),t("div",{class:"line-number"}),t("div",{class:"line-number"}),t("div",{class:"line-number"}),t("div",{class:"line-number"}),t("div",{class:"line-number"}),t("div",{class:"line-number"}),t("div",{class:"line-number"}),t("div",{class:"line-number"}),t("div",{class:"line-number"}),t("div",{class:"line-number"}),t("div",{class:"line-number"}),t("div",{class:"line-number"}),t("div",{class:"line-number"})])],-1),S=t("p",null,[e("원본 Repo에 변화가 있으면 UI상에서 상태가 알려집니다. 이 경우 우측의 "),t("code",null,"Fetch upstream"),e(" 을 통해 "),t("code",null,"Compare"),e("로 변경사항을 확인하거나 "),t("code",null,"Fetch and merge"),e("로 현재의 Repo에 병합할 수 있습니다."),t("br"),t("img",{src:b,alt:"Fetch and merge UI",loading:"lazy"})],-1),q=t("h2",{id:"git-add-and-commit",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#git-add-and-commit"},[t("span",null,[e("git "),t("code",null,"add"),e(" and "),t("code",null,"commit")])])],-1),N=t("p",null,"CLI 컨트롤을 위해서는 앞서 git 유틸 설치가 필요합니다.",-1),M=t("div",{class:"language-bash","data-ext":"sh","data-title":"sh"},[t("pre",{class:"language-bash"},[t("code",null,[t("span",{class:"token comment"},"# 1. 작성한 파일을 git의 관리 대상으로 add 합니다."),e(` +`),t("span",{class:"token function"},"git"),e(),t("span",{class:"token function"},"add"),e(` path/문서.md + +`),t("span",{class:"token comment"},"# 2. 로컬 Repo에서 변경사항을 Commit 합니다."),e(` +`),t("span",{class:"token function"},"git"),e(" commit "),t("span",{class:"token parameter variable"},"-m"),e(),t("span",{class:"token string"},'"커밋메시지를 남깁니다.(예를들어 : 문서를 수정)"'),e(` + +`),t("span",{class:"token comment"},"# 3. 원격 Repo로 변경사항을 Push 합니다."),e(` +`),t("span",{class:"token function"},"git"),e(` push origin main +`)])])],-1),Z=t("p",null,[e("github 웹 환경에서 문서를 추가하고 수정하는 것도 가능합니다."),t("br"),t("img",{src:_,alt:"github ui edit",loading:"lazy"})],-1),V=r('최종적으로 Fork 한 Repo에 docmoa에 기여할 문서가 준비가 된경우 해당 Repo의 github ui에서 커밋된 정보가 있다는 문구와 Contribute
를 클릭하여 Pull request
를 진행 할 수 있습니다. Open pull request
를 클릭하여 Upstream Repo에 요청을 보냅니다.
Pull request를 생성하면 본인 소유의 Repo(Branch)로 부터 docmoa에 지금까지의 변경사항을 병합할 것을 요청할 수 있습니다.
팁
상단에 표기되는 repo의 방향과 branch를 다시한번 확인해주세요.
아래 어떤 항목이 어떻게 변경되는지 내용을 확인할 수 있습니다.
Create pull request
버튼을 클릭하면 디테일한 설명을 추가할 수 잇습니다. 문서 자체의 변경사항만으로 의도를 전달하기 힘든경우 해당 설명이 이해하는데 큰 도움이 됩니다.
이제 Pull request가 받아들여지고나면 docmoa에 기여해주신 내용이 반영됩니다.
Build 주기
2021년 10월 4일 기준 매 5분마다 변경사항이 있으면 docmoa의 빌드가 수행됩니다.
docmoa에 문서 기여하기위한 가이드를 설명합니다. 일반적인 github 상에서의 코드 기여 방식과 동일합니다.
\\n로컬 환경에서 git 명령을 수행하기 위해 설치합니다. github 브라우저 환경에서 수정하는 것도 가능하지만, 로컬에서 문서를 활용하고 오프라인 작업을 위해서는 설치하시기를 권장합니다.
\\nGit 설치 방법 안내를 참고하여 아래 설명합니다.
","autoDesc":true}');export{X as comp,Y as data}; diff --git a/assets/02-SideCar.html-Cll9HLhK.js b/assets/02-SideCar.html-Cll9HLhK.js new file mode 100644 index 0000000000..b7fd077a78 --- /dev/null +++ b/assets/02-SideCar.html-Cll9HLhK.js @@ -0,0 +1,176 @@ +import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{r as l,o as d,c,b as e,d as n,a as i,e as s}from"./app-Bzk8Nrll.js";const r="/assets/02-sidecar-intention-alldeny-ogH-XLae.png",o="/assets/02-sidecar-services-BZgCIlWq.png",v="/assets/02-sidecar-error-VTAGMmH1.png",p="/assets/02-sidecar-topology-blocked-BzutBfyM.png",m="/assets/02-sidecar-topology-allow-F944RXfc.png",u="/assets/02-sidecar-200-BKyjC3GP.png",b={},h=e("h1",{id:"_02-sidecar",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#_02-sidecar"},[e("span",null,"02. SideCar")])],-1),g=e("div",{class:"hint-container tip"},[e("p",{class:"hint-container-title"},"팁"),e("p",null,"실습을 위한 조건은 다음과 같습니다."),e("ul",null,[e("li",null,"Consul 이 구성된 Kubernetes 환경"),e("li",null,[n("설치 구성 시 "),e("code",null,"connectInject"),n(" 이 활성화 되어있어야 합니다.")])])],-1),k={href:"https://learn.hashicorp.com/tutorials/consul/service-mesh-application-secure-networking?in=consul/kubernetes",target:"_blank",rel:"noopener noreferrer"},f=s('Consul 서비스 메시를 사용하면 애플리케이션을 제로 트러스트 네트워크에 배포할 수 있습니다. 제로 트러스트 네트워크는 아무 것도 자동으로 신뢰되지 않는 네트워크입니다. 모든 연결은 인증과 승인을 모두 받아야 합니다. 이 패러다임은 동일한 네트워크에서 다수의 서비스가 실행될 수 있는 마이크로서비스 및 멀티 클라우드 환경에서 중요합니다. Consul 서비스 메시를 사용하면 mTLS를 사용하여 서비스 ID를 인증하고 의도를 사용하여 서비스 작업을 승인하거나 차단할 수 있습니다.
이 튜토리얼에서는 두 개의 서비스 web
및 api
를 Kubernetes 클러스터에서 실행되는 Consul의 서비스 메시에 배포합니다. 두 서비스는 Consul을 사용하여 서로를 검색하고 사이드카 프록시를 사용하여 mTLS를 통해 통신합니다. 두 서비스는 웹UI와 백엔드 서비스로 구성된 간단한 2-tier 애플리케이션으로 HTTP를 통해 서비스와 통신합니다.
Sidecar Proxy는 애플리케이션 컨테이너와 함께 동일 Pod상에 배포됨으로 localhost 로 통신할 수 있습니다. 사용자가 다른서비스에 대한 요청을 Sidecar Proxy에 지정하면 해당 요청을 맵핑된 다른 서비스로 전달합니다. 이 방식은 기존 개발자가 로컬에서 개발하는 환경과 동일하게 동작합니다. UI웹을 로컬 9090포트로 실행하고 백엔드 앱을 8080 포트로 실행한경우 UI웹은 백엔드 앱을 localhost:8080
으로 호출합니다. Consul Sidecar Proxy는 localhost 로 요청되는 포트를 지정한 Upstream
서비스로 전달하는 규칙을 처리하며, 여기에는 mTLS가 자동으로 구성됩니다.
Sidecar Proxy의 서비스 접근 제어를 위해 모든 서비스에 대한 Deny 구성을 수행합니다. UI의 좌측 메뉴의 Intention
을 클릭하고 우측의 Create
버튼을 클릭하여 모든 서비스 (엔터프라이즈의 경우 모든 Namespace 포함) 간에 Deny 규칙을 생성합니다.
테스트 구성을 저장하기 위한 디렉토리를 생성합니다.
mkdir ./k8s_config
+
cat > ./k8s_config/api.yaml <<EOF
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: api-v1
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: api-v1
+spec:
+ selector:
+ app: api-v1
+ ports:
+ - port: 9091
+ targetPort: 9091
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: api-v1
+ labels:
+ app: api-v1
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: api-v1
+ template:
+ metadata:
+ labels:
+ app: api-v1
+ annotations:
+ consul.hashicorp.com/connect-inject: 'true'
+ spec:
+ serviceAccountName: api-v1
+ containers:
+ - name: api
+ image: nicholasjackson/fake-service:v0.7.8
+ ports:
+ - containerPort: 9091
+ env:
+ - name: 'LISTEN_ADDR'
+ value: '127.0.0.1:9091'
+ - name: 'NAME'
+ value: 'api-v1'
+ - name: 'MESSAGE'
+ value: 'Response from API v1'
+EOF
+
cat > ./k8s_config/web.yaml <<EOF
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: web
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: web
+spec:
+ selector:
+ app: web
+ ports:
+ - port: 9090
+ targetPort: 9090
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: web-deployment
+ labels:
+ app: web
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: web
+ template:
+ metadata:
+ labels:
+ app: web
+ annotations:
+ consul.hashicorp.com/connect-inject: 'true'
+ consul.hashicorp.com/connect-service-upstreams: 'api-v1:9091'
+ spec:
+ serviceAccountName: web
+ containers:
+ - name: web
+ image: nicholasjackson/fake-service:v0.7.8
+ ports:
+ - containerPort: 9090
+ env:
+ - name: 'LISTEN_ADDR'
+ value: '0.0.0.0:9090'
+ - name: 'UPSTREAM_URIS'
+ value: 'http://localhost:9091'
+ - name: 'NAME'
+ value: 'web'
+ - name: 'MESSAGE'
+ value: 'Hello World'
+EOF
+
프론트엔드 서비스에 Deployment
구성 내용의 annotation
을 확인하세요. 이 형식은 9091로 요청된 localhost로의 요청을 sidecar가 api-v1
서비스로 전달하는 것을 의미합니다.
consul.hashicorp.com/connect-service-upstreams: 'api-v1:9091'
kubectl apply
명령을 통해 배포를 확인하고 Consul UI에서 확인합니다.
kubectl apply -f ./k8s_config/api.yaml
+
# 출력
+serviceaccount/api-v1 created
+service/api-v1 created
+deployment.apps/api-v1 created
+
kubectl apply -f ./k8s_config/web.yaml
+
# 출력
+serviceaccount/web created
+service/web created
+deployment.apps/web-deployment created
+
UI에 접속하고 좌측 Namespace에서 사용중인 Namespace를 확인합니다. 서비스 목록에 api-v1
, web
이 등록되고 상태가 정상임을 확인합니다.
port-forward
를 통해 로컬에서 web 앱을 확인합니다.
kubectl port-forward service/web 9090:9090 --address 0.0.0.0
+
# 출력
+Forwarding from 0.0.0.0:9090 -> 9090
+
500 에러가 발생하였습니다. Consul Service Mesh는 서비스간 의도적으로 접속 가능여부를 동적으로 통제합니다. 이 기능을 Intention
이라고 부릅니다. Consul UI에 접속하여 web
서비스 이름을 클릭하면 다음과 같이 요청이 거부되어 있는것을 확인할 수 있습니다.
Intention 수정을 위해서는 권한이 필요합니다. 현재 환경에서는 전달받은 token (3108cbb3-005c-a3e4-9a42-6f13d1f5e4e6) 을 우측 상단 로그인에서 입력합니다.
x
표시를 클릭하여 Create
버튼을 클릭하여 Intention 규칙을 생성합니다. 이후에 연결이 허용된것을 확인할 수 있습니다.
다음 과정을 진행하기 위해 기존 적용된 구성을 삭제합니다.
kubectl delete -f ./k8s_config
+
# 출력
+serviceaccount "api-v1" deleted
+service "api-v1" deleted
+deployment.apps "api-v1" deleted
+serviceaccount "web" deleted
+service "web" deleted
+deployment.apps "web-deployment" deleted
+
백엔드 서비스를 다른 Namespace에 배포하고, 프론트엔드가 해당 백엔드에 접근할 수 있도록 수정합니다.
cat > ./k8s_config/web.yaml <<EOF
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: web
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: web
+spec:
+ selector:
+ app: web
+ ports:
+ - port: 9090
+ targetPort: 9090
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: web-deployment
+ labels:
+ app: web
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: web
+ template:
+ metadata:
+ labels:
+ app: web
+ annotations:
+ consul.hashicorp.com/connect-inject: 'true'
+ consul.hashicorp.com/connect-service-upstreams: 'api-v1.<namespace>:9091'
+ spec:
+ serviceAccountName: web
+ containers:
+ - name: web
+ image: nicholasjackson/fake-service:v0.7.8
+ ports:
+ - containerPort: 9090
+ env:
+ - name: 'LISTEN_ADDR'
+ value: '0.0.0.0:9090'
+ - name: 'UPSTREAM_URIS'
+ value: 'http://localhost:9091'
+ - name: 'NAME'
+ value: 'web'
+ - name: 'MESSAGE'
+ value: 'Hello World'
+EOF
+
프론트엔드 서비스에 Deployment
구성 내용의 annotation
을 확인하세요. 이 형식은 9091로 요청된 localhost로의 요청을 sidecar가 <namespace>
Namespace의 api-v1
서비스로 전달하는 것을 의미합니다.
consul.hashicorp.com/connect-service-upstreams: 'api-v1.<namespace>:9091'
또한 Namespace 간 Intention을 작성하여 적용해야합니다.
참고 - Intention 순위
범위가 좁을수록 우선순위가 높습니다.
Source Namespace | Source Name | Destination Namespace | Destination Name | 높을수록 서열 높음 |
---|---|---|---|---|
Exact | Exact | Exact | Exact | 9 |
Exact | * | Exact | Exact | 8 |
* | * | Exact | Exact | 7 |
Exact | Exact | Exact | * | 6 |
Exact | * | Exact | * | 5 |
* | * | Exact | * | 4 |
Exact | Exact | * | * | 3 |
Exact | * | * | * | 2 |
* | * | * | * | 1 |
팁
\\n실습을 위한 조건은 다음과 같습니다.
\\nconnectInject
이 활성화 되어있어야 합니다.\\n\\n"}');export{A as comp,P as data}; diff --git a/assets/02-Thanks.html-BWdU5ZFB.js b/assets/02-Thanks.html-BWdU5ZFB.js new file mode 100644 index 0000000000..6a54dd71bc --- /dev/null +++ b/assets/02-Thanks.html-BWdU5ZFB.js @@ -0,0 +1 @@ +import{_ as c}from"./plugin-vue_export-helper-DlAUqK2U.js";import{i as l,j as u,o,c as e,b as t,F as h,g as d,d as p,t as m}from"./app-Bzk8Nrll.js";const g={props:{owner:{type:String,required:!0},repo:{type:String,required:!0}},setup(s){const a=l([]);return u(async()=>{try{const r=await fetch("https://api.github.com/repos/docmoa/docs/contributors");a.value=await r.json()}catch(r){console.error("Failed to fetch contributors:",r)}}),{contributors:a}}},_=t("h1",{id:"thank-you",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#thank-you"},[t("span",null,"Thank you")])],-1),b=t("h3",null,"Contributors:",-1),y={key:0},k=["href"],f=["src"],T={key:1};function v(s,a,r,i,x,w){return o(),e("div",null,[_,t("div",null,[b,i.contributors.length?(o(),e("ul",y,[(o(!0),e(h,null,d(i.contributors,n=>(o(),e("li",{key:n.id},[t("a",{href:n.html_url,target:"_blank"},[t("img",{src:n.avatar_url,alt:"avatar",width:"30"},null,8,f),p(" "+m(n.login),1)],8,k)]))),128))])):(o(),e("p",T,"Loading contributors..."))])])}const C=c(g,[["render",v],["__file","02-Thanks.html.vue"]]),j=JSON.parse('{"path":"/99-about/02-Thanks.html","title":"Thank you","lang":"ko-KR","frontmatter":{"description":"Thank you Contributors: avatar {{ contributor.login }} Loading contributors... ","head":[["meta",{"property":"og:url","content":"https://docmoa.github.io/99-about/02-Thanks.html"}],["meta",{"property":"og:site_name","content":"docmoa"}],["meta",{"property":"og:title","content":"Thank you"}],["meta",{"property":"og:description","content":"Thank you Contributors: avatar {{ contributor.login }} Loading contributors... "}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"ko-KR"}],["meta",{"property":"og:updated_time","content":"2023-09-19T04:54:16.000Z"}],["meta",{"property":"article:modified_time","content":"2023-09-19T04:54:16.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Thank you\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2023-09-19T04:54:16.000Z\\",\\"author\\":[]}"]]},"headers":[],"git":{"createdTime":1628085698000,"updatedTime":1695099256000,"contributors":[{"name":"Great-Stone","email":"hahohh@gmail.com","commits":3}]},"readingTime":{"minutes":0.3,"words":89},"filePathRelative":"99-about/02-Thanks.md","localizedDate":"2021년 8월 4일","excerpt":"\\n
Loading contributors...
\\n이번에 연재할 스터디는 AWS EKS Workshop Study (=AEWS)이다. AWS에서 공식적으로 제공되는 다양한 HOL 기반의 Workshop과 가시다님의 팀에서 2차 가공한 컨텐츠를 기반으로 진행한다.
2주차 부터는 원클릭으로 EKS 실습환경을 배포할 수 있는 코드를 사용한다. 필자는 사용중인 AWS IAM 권한 제약사항으로 기존 CF 코드를 변경하여 베스천용 EC2에 관리자 권한을 위임하여 배포할 예정이다.
',5),u={href:"https://cloudkatha.com/attach-an-iam-role-to-an-ec2-instance-with-cloudformation/",target:"_blank",rel:"noopener noreferrer"},d=t(`# YAML 파일 다운로드
+curl -O https://gist.githubusercontent.com/hyungwook0221/238d96b3b751362cc03ea40494d15313/raw/49de0a9056688b206a41349fc90727d2375f4f02/aews-eks-oneclick-with-ec2-profile.yaml
+
+# CloudFormation 스택 배포
+# aws cloudformation deploy --template-file eks-oneclick.yaml --stack-name myeks --parameter-overrides KeyName=<My SSH Keyname> SgIngressSshCidr=<My Home Public IP Address>/32 MyIamUserAccessKeyID=<IAM User의 액세스키> MyIamUserSecretAccessKey=<IAM User의 시크릿 키> ClusterBaseName='<eks 이름>' --region ap-northeast-2
+예시) aws cloudformation deploy --template-file eks-oneclick.yaml --stack-name myeks --parameter-overrides KeyName=hw-key SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32 ClusterBaseName=myeks --region ap-northeast-2 --capabilities CAPABILITY_NAMED_IAM
+
+# CloudFormation 스택 배포 완료 후 작업용 EC2 IP 출력
+aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text
+
+# 마스터노드 SSH 접속
+ssh -i ~/.ssh/kp-gasida.pem ec2-user@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)
+
일반적으로 Calico와 같은 K8s CNI의 경우는 Node - Pod의 IP 대역이 다르지만 AWS VPC CNI의 경우에는 Node-Pod 대역을 동일하게 해서 통신이 가능하도록 구성할 수 있다.
일반적으로 Outer 패킷을 감싸서 오버레이로 통신하지만 AWS VPC CNI는 오히려 심플한 구조를 가진다. 이로인해 간단하고 효율적인 통신이 가능하다!
K8s 환경에서는 내/외부 통신을 위한 서비스를 크게 3가지 형태로 제공한다.
필자는 그 중에서 LoadBalancer 타입을 AWS 환경에서 어떻게 활용할 수 있는지를 집중적으로 확인하고 Consul 샘플 예제와 함께 적용해볼 예정이다.
LoadBalancer 배포 시 NLB 모드는 다음 두 가지 유형을 사용할 수 있다.
externalTrafficPolicy
: ClusterIP ⇒ 2번 분산 및 SNAT으로 Client IP 확인 불가능 ← LoadBalancer
타입 (기본 모드) 동작externalTrafficPolicy
: Local ⇒ 1번 분산 및 ClientIP 유지, 워커 노드의 iptables 사용함참고 : 반드시 AWS LoadBalancer 컨트롤러 파드 및 정책 설정이 필요함!
Proxy Protocol v2 비활성화
⇒ NLB에서 바로 파드로 인입, 단 ClientIP가 NLB로 SNAT 되어 Client IP 확인 불가능Proxy Protocol v2 활성화
⇒ NLB에서 바로 파드로 인입 및 ClientIP 확인 가능(→ 단 PPv2 를 애플리케이션이 인지할 수 있게 설정 필요)# AWSLoadBalancerControllerIAMPolicy 생성
+curl -o iam_policy.json https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.4.7/docs/install/iam_policy.json
+aws iam create-policy --policy-name AWSLoadBalancerControllerIAMPolicy --policy-document file://iam_policy.json
+
+# 업데이트가 필요한 경우
+# aws iam update-policy --policy-name AWSLoadBalancerControllerIAMPolicy --policy-document file://iam_policy.json
+
+# AWS Load Balancer Controller를 위한 ServiceAccount를 생성
+eksctl create iamserviceaccount --cluster=$CLUSTER_NAME --namespace=kube-system --name=aws-load-balancer-controller \\
+--attach-policy-arn=arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy --override-existing-serviceaccounts --approve
+
+## IRSA 정보 확인
+eksctl get iamserviceaccount --cluster $CLUSTER_NAME
+
+## 서비스 어카운트 확인
+kubectl get serviceaccounts -n kube-system aws-load-balancer-controller -o yaml | yh
+
+# Helm Chart 설치
+helm repo add eks https://aws.github.io/eks-charts
+helm repo update
+helm install aws-load-balancer-controller eks/aws-load-balancer-controller -n kube-system --set clusterName=$CLUSTER_NAME \\
+ --set serviceAccount.create=false --set serviceAccount.name=aws-load-balancer-controller
+
+# 설치 확인
+kubectl get crd
+kubectl get deployment -n kube-system aws-load-balancer-controller
+kubectl describe deploy -n kube-system aws-load-balancer-controller | grep 'Service Account'
+ Service Account: aws-load-balancer-controller
+
+# 클러스터롤, 클러스터 롤바인딩 확인
+kubectl describe clusterrolebindings.rbac.authorization.k8s.io aws-load-balancer-controller-rolebinding
+kubectl describe clusterroles.rbac.authorization.k8s.io aws-load-balancer-controller-role
+
AWS LoadBalancer Controller가 동작하기 위해 필요한 SA를 생성 후 연결된 ClusterRole과 ClusterRoleBinding을 화인
LoadBalancer 타입의 서비스와 및 파드를 배포하고 NLB 모드에 따라서 Client IP가 어떻게 확인되는지 확인해본다.
# 모니터링
+watch -d kubectl get pod,svc,ep
+
+# 작업용 EC2 - 디플로이먼트 & 서비스 생성
+curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/2/echo-service-nlb.yaml
+cat echo-service-nlb.yaml | yh
+kubectl apply -f echo-service-nlb.yaml
+
+# 파드 로깅 모니터링
+kubectl logs -l app=deploy-websrv -f
+
+# 분산 접속 확인
+NLB=$(kubectl get svc svc-nlb-ip-type -o jsonpath={.status.loadBalancer.ingress[0].hostname})
+curl -s $NLB
+
NLB에 등록된 Target IP 정보는 생성된 샘플 Pod의 IP인 것을 확인할 수 있다.
이제 NLB를 통해서 Pod를 호출할 경우 Client IP가 어떻게 확인되는지 확인해보자.
다음 정보는 각 Node의 정보가 아닌 다른 IP 정보가 확인된다.
그렇다면 Client IP의 정체는? 바로 NLB에 할당된 네트워크 인터페이스의 IP 이다.
이제 실제로 Client IP를 추적하기 위한 방법을 알아본다.
앞선 실습에서 NLB로 SNAT되어서Client IP 확인되지 못하는 것을 확인하였다. 이번에는 Proxy Protocol v2을 활성화 하여 IP 정보를 유지하는 방법을 알아본다. (이미지 출처 : 가시다님 스터디)
이때 중요한 부분은 SVC 생성 시 service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: "*"
어노테이션을 활성화 하는 것이다.
# 생성
+cat <<EOF | kubectl create -f -
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: gasida-web
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: gasida-web
+ template:
+ metadata:
+ labels:
+ app: gasida-web
+ spec:
+ terminationGracePeriodSeconds: 0
+ containers:
+ - name: gasida-web
+ image: gasida/httpd:pp
+ ports:
+ - containerPort: 80
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: svc-nlb-ip-type-pp
+ annotations:
+ service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
+ service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
+ service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
+ service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: "*"
+spec:
+ ports:
+ - port: 80
+ targetPort: 80
+ protocol: TCP
+ type: LoadBalancer
+ loadBalancerClass: service.k8s.aws/nlb
+ selector:
+ app: gasida-web
+EOF
+
+---
+
+# apache에 proxy protocol 활성화 확인
+kubectl exec deploy/gasida-web -- apachectl -t -D DUMP_MODULES
+kubectl exec deploy/gasida-web -- cat /usr/local/apache2/conf/httpd.conf
+
+# 접속 확인
+NLB=$(kubectl get svc svc-nlb-ip-type-pp -o jsonpath={.status.loadBalancer.ingress[0].hostname})
+curl -s $NLB
+
+# 지속적인 접속 시도
+while true; do curl -s --connect-timeout 1 $NLB; echo "----------" ; date "+%Y-%m-%d %H:%M:%S" ; sleep 1; done
+
+# 로그 확인
+kubectl logs -l app=gasida-web -f
+
IP를 확인해본 결과 동일한 공인 IP로 찍히는 것으로 확인된다.
그렇다면 해당 IP는 무엇일까? 바로 현재 curl -s
명령을 수행한 Bastion 노드의 정보이다.
이렇게 NLB를 통해 호출하더라도 정상적으로 Client IP를 유지하는 방법을 알아보았다. 실제로 온프레미스 환경에서 3-Tier 기반의 WEB/WAS를 구성하다 보면 Client IP를 유지하기 위해 XFF 설정을 하는 것이 일반적이다. 다만, NLB의 경우에는 L4 계층까지만 패킷에 대한 이해와 분석이 가능하므로 Proxy Protocol을 써야한다는 새로운 정보를 알 수 있는 좋은 기회였다.
다음 예제는 Consul IngressGateway를 통한 ServiceMesh의 단일 진입점을 테스트해볼 예정이다. Consul 1.15x 버전에는 Envoy의 Access Log 기능이 추가되어 이번 스터디를 통해 학습한 NLB의 IP 유지방안에 대한 테스트를 진행해본다.
`,34),v=n("p",null,"참고 : Consul Gateway에서 envoy access log 활성화 기능",-1),b={href:"https://developer.hashicorp.com/consul/docs/connect/observability/access-logs",target:"_blank",rel:"noopener noreferrer"},g={href:"https://github.com/hashicorp/consul/issues/5231",target:"_blank",rel:"noopener noreferrer"},h={href:"https://github.com/hashicorp/consul/pull/15864",target:"_blank",rel:"noopener noreferrer"},y=t(`처음 설정시에는 PPv2를 사용하지 않고 NLB를 적용해볼 예정이다. => Client IP가 어떻게 찍히는지 확인!
client:
+ grpc: true
+connectInject:
+ consulNamespaces:
+ mirroringK8S: true
+ enabled: true
+controller:
+ enabled: true
+global:
+ acls:
+ manageSystemACLs: true
+ enableConsulNamespaces: true
+ enterpriseLicense:
+ secretKey: key
+ secretName: license
+ gossipEncryption:
+ autoGenerate: true
+ image: hashicorp/consul-enterprise:1.15.1-ent
+ #imageEnvoy: envoyproxy/envoy:v1.22.5
+ #imageK8S: hashicorp/consul-k8s-control-plane:0.49.5
+ metrics:
+ enabled: false
+ tls:
+ enableAutoEncrypt: true
+ enabled: true
+ httpsOnly: false
+ verify: false
+ingressGateways:
+ defaults:
+ replicas: 1
+ service:
+ type: LoadBalancer
+ annotations: |
+ service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
+ service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
+ service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
+ #service.beta.kubernetes.io/aws-load-balancer-healthcheck-port: "8080"
+ enabled: true
+ gateways:
+ - name: ingress-gateway
+meshGateway:
+ enabled: false
+ replicas: 1
+ service:
+ enabled: true
+ nodePort: 32000
+ type: NodePort
+server:
+ replicas: 3
+terminatingGateways:
+ defaults:
+ replicas: 1
+ enabled: false
+ui:
+ enabled: true
+ service:
+ port:
+ http: 80
+ https: 443
+ type: LoadBalancer
+
apiVersion: consul.hashicorp.com/v1alpha1
+kind: IngressGateway
+metadata:
+ name: ingress-gateway
+spec:
+ listeners:
+ - port: 8080
+ protocol: http
+ services:
+ - name: static-server
+
spec.accessLogs
를 통해 AccessLog 활성화 및 파일경로 추가
apiVersion: consul.hashicorp.com/v1alpha1
+kind: ProxyDefaults
+metadata:
+ name: global
+spec:
+ accessLogs:
+ enabled: true
+# type: file
+# path: "/var/log/envoy/access-logs.txt"
+
apiVersion: consul.hashicorp.com/v1alpha1
+kind: ServiceDefaults
+metadata:
+ name: static-server
+spec:
+ protocol: http
+
apiVersion: consul.hashicorp.com/v1alpha1
+kind: ServiceIntentions
+metadata:
+ name: static-server
+spec:
+ destination:
+ name: static-server
+ sources:
+ - name: ingress-gateway
+ action: allow
+
apiVersion: v1
+kind: Service
+metadata:
+ name: static-server
+spec:
+ selector:
+ app: static-server
+ ports:
+ - protocol: TCP
+ port: 80
+ targetPort: 8080
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: static-server
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: static-server
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: static-server
+ template:
+ metadata:
+ name: static-server
+ labels:
+ app: static-server
+ annotations:
+ 'consul.hashicorp.com/connect-inject': 'true'
+ spec:
+ containers:
+ - name: static-server
+ image: hashicorp/http-echo:latest
+ args:
+ - -text="hello world"
+ - -listen=:8080
+ ports:
+ - containerPort: 8080
+ name: http
+ serviceAccountName: static-server
+
EXTERNAL_IP=$(kubectl get services --selector component=ingress-gateway --output jsonpath="{range .items[*]}{@.status.loadBalancer.ingress[*].hostname}{end}")
+echo "Connecting to \\"$EXTERNAL_IP\\""
+curl --header "Host: static-server.ingress.consul" "http://$EXTERNAL_IP:8080"
+
호출결과 앞서 실습에서 확인해본 것과 동일하게 NLB IP Target & Proxy Protocol v2 비활성화 일 경우에는 로드밸런서 인터페이스 IP가 확인된다.
이번에는 위와 동일하지만 NLB의 어노테이션만 PPv2를 활성화 한다.
service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: "*"
#(생략)
+ingressGateways:
+ defaults:
+ replicas: 1
+ service:
+ type: LoadBalancer
+ annotations: |
+ service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
+ service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
+ service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
+ service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: "*"
+ enabled: true
+ gateways:
+ - name: ingress-gateway
+#(생략)
+
위와 동일하게 사용
EXTERNAL_IP=$(kubectl get services --selector component=ingress-gateway --output jsonpath="{range .items[*]}{@.status.loadBalancer.ingress[*].hostname}{end}")
+echo "Connecting to \\"$EXTERNAL_IP\\""
+curl --header "Host: static-server.ingress.consul" "http://$EXTERNAL_IP:8080"
+
하지만 PPv2 설정 후 static-server 앱을 테스트해본 결과 정상적으로 동작하지 않는 것으로 보인다.
위와 관련해서 확인해본 결과 Istio의 경우에는 EnvoyFilter
등을 통해 해결하는 방안(?)이 있는 것으로 보이며, 일반적으로 PPv2를 사용하기 위해서는 애플리케이션 단에서 사용할 수 있도록 설정이 필요한 것으로 확인되었다.
이번에 연재할 스터디는 AWS EKS Workshop Study (=AEWS)이다. AWS에서 공식적으로 제공되는 다양한 HOL 기반의 Workshop과 가시다님의 팀에서 2차 가공한 컨텐츠를 기반으로 진행한다.
\\n\\n톰켓의 버전이 올라감에 따라 지원하는 Java Standard Spec Version 또한 변경됩니다. 이 경우 일부 상위 버전은 JDK의 특정 버전에서 지원되지 않을 수 있지요. 따라서 개발되는 어플리케이션의 JDK요구치나 표준화된 톰캣 버전에 따라 지원되는 JDK 버전이 상이할 수 있습니다. 다음의 표를 참고하시기 바랍니다.
Servlet Spec | JSP Spec | EL Spec | WebSocket Spec | Authentication (JASPIC) Spec | Apache Tomcat Version | Latest Released Version | Supported Java Versions |
---|---|---|---|---|---|---|---|
6.0 | 3.1 | 5.0 | 2.1 | 3.0 | 10.1.x | 10.1.0-M8 (alpha) | 11 and later |
5.0 | 3.0 | 4.0 | 2.0 | 2.0 | 10.0.x | 10.0.14 | 8 and later |
4.0 | 2.3 | 3.0 | 1.1 | 1.1 | 9.0.x | 9.0.56 | 8 and later |
3.1 | 2.3 | 3.0 | 1.1 | 1.1 | 8.5.x | 8.5.73 | 7 and later |
3.1 | 2.3 | 3.0 | 1.1 | N/A | 8.0.x (superseded) | 8.0.53 (superseded) | 7 and later |
3.0 | 2.2 | 2.2 | 1.1 | N/A | 7.0.x (archived) | 7.0.109 (archived) | 6 and later (7 and later for WebSocket) |
2.5 | 2.1 | 2.1 | N/A | N/A | 6.0.x (archived) | 6.0.53 (archived) | 5 and later |
2.4 | 2.0 | N/A | N/A | N/A | 5.5.x (archived) | 5.5.36 (archived) | 1.4 and later |
2.3 | 1.2 | N/A | N/A | N/A | 4.1.x (archived) | 4.1.40 (archived) | 1.3 and later |
2.2 | 1.1 | N/A | N/A | N/A | 3.3.x (archived) | 3.3.2 (archived) | 1.1 and later |
톰캣 5.5.x 버전의 경우 5.5.12 버전 이후로는 JDK 5 이상을 지원함에 유의합니다.
Java SE의 경우 OS 플랫폼에 따라 제공하는 벤더가 다른 경우가 있습니다. Oracle이 서브스크립션 형태로, 업데이트에 대해 유료화 선언을 한 이후로 여러 파생 Java를 고려할 수 있습니다. 여전히 8 버전을 사용하는 서비스가 많아 OracleJDK가 점유율이 높으나, 이후 높은 버전으로 이전시에는 다른 JDK를 고려하는 상황도 발생할 것으로 보입니다.
Most Popular JRE/JDK Distribution (JRebel, 2020)
톰캣을 설치하는 OS 플랫폼 환경은 모든 환경을 지원합니다. 그나마 예전에는 일부 Unix/Linux/OSX 환경에서 Apache HTTP Server 설치하듯 컴파일을 통해 구성하였으나, 최근에는 압축파일을 해제하고 바로 사용할 수 있는 경우가 대부분입니다.
\\n톰캣을 운영하기 위해 OS를 선택해야하는 입장이라면 다음과 같은 설치 타입을 고려할 수 있습니다.
Terraform에서는 당연히 동작해야하는 필연적 기능이기 때문에 오픈소스를 포함하여 모든 유형의 Terraform에서 제공되는 기능입니다.
유형 | 지원여부 |
---|---|
Terraform OSS (Open Source Software) | ✔︎ |
Terraform Cloud | ✔︎ |
Terraform Cloud for Business | ✔︎ |
Terraform Enterprise | ✔︎ |
Infrastructure as Code에 대해 간단히 소개하자면 수작업으로 프로비저닝 하던 방식, 예를 들면 UI 클릭이나 개별적인 스크립트를 사용하여 프로비저닝하는 방식은 자동화하기 어렵고, 충분히 숙달되지 않거나 밤샘작업과 스트레스로 잠시 집중력이 떨어지면 실수가 발생할 수 있습니다. 그리고 스크립트 방식은 나름 잘 정의되어있지만 순차적으로 수행되고 중간에 오류가 나면 다시 돌이키기 힘든 방식이였습니다.
Terraform에서는 이전의 프로비저닝 방식을 개선하여 좀더 안정적인고 체계적인 관리 방식을 도입할 수 있는 메커니즘과 Infrastructure as Code의 핵심인 Code를 잘 만들고 관리할 수 있는 도구를 제공합니다.
기본적으로 Terraform은 HCL이라고하는 HashiCorp Configuration Language와 JSON가 코드의 영역을 담당하고 있습니다. 특히 HCL은 쉽게 읽을수 있고 빠르게 배울 수 있는 언어의 특성을 가지고 있습니다.
인프라가 코드로 표현되고, 이 코드는 곧 인프라이기 때문에 선언적 특성을 갖게 되고 튜링 완전한 언어적 특성을 갖습니다. 즉, 일반적인 프로그래밍 언어의 일반적인 조건문 처리같은 동작이 가능하다는 것입니다.
이렇게 코드화된 인프라는 주 목적인 자동화와 더불어 쉽게 버저닝하여 히스토리를 관리하고 함께 작업할 수 있는 기반을 제공하게 됩니다.
사실 이미 테라폼을 조금이라도 써보신분들은 당연하게도 HCL을 써보셨을 수도 있지만 처음 접하시는 경우 뭔가 또 배워야 하는건가? 어려운건가? 라는 마음의 허들이 생길 수 있습니다.
앞서 설명드렸듯 HCL은 JSON과 호환되고 이런 방식이 더 자연스러우신 분들은 JSON으로 관리가 가능합니다. 하지만 HCL에 대한 일반적인 질문은 왜 JSON이나 YAML같은 방식이 아닌지 입니다.
HCL 이전에 HashiCorp에서 사용한 도구는 Ruby같은 여타 프로그래밍 언어를 사용하여 JSON같은 구조를 만들어내기 위해 다양한 언어를 사용했습니다. 이때 알게된 점은 어떤 사용자는 인간 친화적인 언어를 원했고 어떤 사람들은 기계 친화적 언어를 원한다는 것입니다.
JSON은 이같은 요건 양쪽에 잘 맞지만 상당히 구문이 길어지고 주석이 지원되지 않는 다는 단점이 있습니다. YAML을 사용하면 처음 접하시는 분들은 실제 구조를 만들어내고 익숙해지는데 어려움이 발생하였습니다. 더군다나 일반적 프로그래밍 언어는 허용하지 않아야하는 많은 기능을 내장하고 있는 문제점도 있었습니다.
이런 여러 이유들로 JSON과 호환되는 자체 구성언어를 만들게 되었고 HCL은 사람이 쉽게 작성하고 수정할 수 있도록 설계되었습니다. 덩달아 HCL용 API가 JSON을 함께 호환하기 때문에 기계 친화적이기도 합니다.
HCL을 익히고 사용하는 건 어떨까요?
예를 들어 Python 코드로 비슷하게 정의를 내려보았습니다. 우리가 사용할 패키지를 Import하고 해당 패키지가 기본적으로 필요로하는 값을 넣어 초기화 합니다. 여기서는 aws
라는 패키지에 region
과 profile
이름을 넣어서 기본적으로 동작 할 수 있는 설정으로 초기화 하였습니다. 이후에 해당 패키지가 동작할 수 있는 여러 서브 펑션들에 대한 정의를 하고 마지막으로는 실행을 위한 큐에 넣습니다.
HCL도 거의 이런 일반적 프로그래밍의 논리와 비슷합니다. 우선 사용할 프로바이더 라고하는 마치 라이브러리나 패키지 같은 것을 정의 합니다. 이 프로바이더에는 기본적으로 선언해주어야 하는 값들이 있습니다.
그리고 이 프로바이더가 제공하는 기본적인 모듈들, 즉, 클래스나 구조체와 비슷한 형태로 정의 합니다. resource
에 대한 정의는 마치 aws_instance
라는 클래스를 example
로 정의하는 것과 비슷한 메커니즘을 갖습니다. 그리고 해당 리소스의 값들을 사용자가 재정의 하는 방식입니다.
// 한줄 주석 방법1
+# 한줄 주석 방법2
+
+/*
+다중
+라인
+주석
+*/
+
+locals {
+ key1 = "value1" // = 를 기준으로 키와 값이 구분되며
+ key2 = "value2" // = 양쪽의 공백은 중하지 않습니다.
+ myStr = "TF ♡ UTF-8" // UTF-8 문자를 지원합니다.
+ multiStr = <<FOO // <<EOF 같은 여러줄의 스트링을 지원합니다.
+ Multi
+ Line
+ String
+ with <<ANYTEXT
+ FOO // 앞과 끝 문자만 같으면 됩니다.
+
+ boolean1 = true // boolean true
+ boolean2 = false // boolean false를 지원합니다.
+
+ deciaml = 123 // 기본적으로 숫자는 10진수,
+ octal = 0123 // 0으로 시작하는 숫자는 8진수,
+ hexadecimal = "0xD5" // 0x 값을 포함하는 스트링은 16진수,
+ scientific = 1e10 // 과학표기 법도 지원합니다.
+
+ //funtion 들이 많이 준비되어있습니다.
+ myprojectname = format("%s is myproject name", var.project)
+ //인라인 조건문도 지원합니다.
+ credentials = var.credentials == "" ? file(var.credentials_file) : var.credentials
+}
+
Terraform의 가장 주요한 기능으로 Infrastructure as Code 를 이야기 할 수 있습니다. 그리고 이를 지원하는 HCL에 대해 알아보고자 합니다.
\\n"}');export{H as comp,L as data}; diff --git a/assets/02-intention-alldeny-CpsNCiFl.png b/assets/02-intention-alldeny-CpsNCiFl.png new file mode 100644 index 0000000000..fa6dd3c60f Binary files /dev/null and b/assets/02-intention-alldeny-CpsNCiFl.png differ diff --git a/assets/02-intention-crd-DDJBL_BF.png b/assets/02-intention-crd-DDJBL_BF.png new file mode 100644 index 0000000000..227c9b5ee5 Binary files /dev/null and b/assets/02-intention-crd-DDJBL_BF.png differ diff --git a/assets/02-jobs.html-Cq6dIiBF.js b/assets/02-jobs.html-Cq6dIiBF.js new file mode 100644 index 0000000000..a3de1b0abc --- /dev/null +++ b/assets/02-jobs.html-Cq6dIiBF.js @@ -0,0 +1,140 @@ +import{_ as o,a as p,b as c,c as d,d as r,e as u}from"./1563945539114-BeDUHaoS.js";import{_ as v}from"./plugin-vue_export-helper-DlAUqK2U.js";import{r as i,o as m,c as b,a as s,b as n,d as e,e as t}from"./app-Bzk8Nrll.js";const g={},k=n("h1",{id:"_2-jobs",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#_2-jobs"},[n("span",null,"2. Jobs")])],-1),h=n("p",null,"프로젝트는 Job의 일부 입니다. 즉, 모든 프로젝트가 Job이지만 모든 Job이 프로젝트는 아닙니다. Job의 구조는 다음과 같습니다.",-1),f=t(`FreeStyleProejct, MatrixProject, ExternalJob만 New job
에 표시됩니다.
Step 1에서는 stage
없이 기본 Pipeline을 실행하여 수행 테스트를 합니다.
Jenkins 로그인
좌측 새로운 Item
클릭
Enter an item name
에 Job 이름 설정 (e.g. 2.Jobs)
Pipeline
선택 후 OK
버튼 클릭
Pipeline
항목 오른 쪽 Try sample Pipelie...
클릭하여 Hello world
클릭 후 저장
node {
+ echo 'Hello World'
+}
+
좌측 Build now
클릭
좌측 Build History
의 최근 빌드된 항목(e.g. #1) 우측에 마우스를 가져가면 dropdown 버튼이 생깁니다. 해당 버튼을 클릭하여 Console Output
클릭
수행된 echo
동작 출력을 확인합니다.
Started by user GyuSeok.Lee
+Running in Durability level: MAX_SURVIVABILITY
+[Pipeline] Start of Pipeline
+[Pipeline] node
+Running on Jenkins in /var/lib/jenkins/workspace/2.Jobs
+[Pipeline] {
+[Pipeline] echo
+Hello World
+[Pipeline] }
+[Pipeline] // node
+[Pipeline] End of Pipeline
+Finished: SUCCESS
+
Step 2에서는 stage
를 구성하여 실행합니다.
기존 생성한 Job 클릭 (e.g. 02-02.Jobs)
좌측 구성
을 클릭하여 Pipeline
스크립트를수정합니다.
pipeline{
+ agent any
+ stages {
+ stage("Hello") {
+ steps {
+ echo 'Hello World'
+ }
+ }
+ }
+}
+
수정 후 좌측 Build Now
를 클릭하여 빌드 수행 후 결과를 확인합니다.
Step 1
에서의 결과와는 달리 Stage View
항목과 Pipeline stage가 수행된 결과를 확인할 수 있는 UI가 생성됩니다.
수행된 빌드의 Console Output
을 확인하면 앞서 Step 1
에서는 없던 stage 항목이 추가되어 수행됨을 확인 할 수 있습니다.
Started by user GyuSeok.Lee
+Running in Durability level: MAX_SURVIVABILITY
+[Pipeline] Start of Pipeline
+[Pipeline] node
+Running on Jenkins in /var/lib/jenkins/workspace/2.Jobs
+[Pipeline] {
+[Pipeline] stage
+[Pipeline] { (Hello)
+[Pipeline] echo
+Hello World
+[Pipeline] }
+[Pipeline] // stage
+[Pipeline] }
+[Pipeline] // node
+[Pipeline] End of Pipeline
+Finished: SUCCESS
+
Pipeline 내에서 사용되는 매개변수 정의를 확인해 봅니다. Pipeline 스크립트는 다음과 같습니다.
pipeline {
+ agent any
+ parameters {
+ string(name: 'Greeting', defaultValue: 'Hello', description: 'How should I greet the world?')
+ }
+ stages {
+ stage('Example') {
+ steps {
+ echo "\${params.Greeting} World!"
+ }
+ }
+ }
+}
+
parameters
항목내에 매개변수의 데이터 유형(e.g. string)을 정의합니다. name
은 값을 담고있는 변수이고 defaultValue
의 값을 반환합니다. Pipeline에 정의된 parameters
는 params
내에 정의 되므로 \${params.매개변수이름}
과 같은 형태로 호출 됩니다.
저장 후 다시 구성
을 확인하면 이 빌드는 매개변수가 있습니다
가 활성화 되고 내부에 추가된 매개변수 항목을 확인 할 수 있습니다.
이렇게 저장된 Pipeline Job은 매개변수를 외부로부터 받을 수 있습니다. 따라서 좌측의 기존 Build Now
는 build with Parameters
로 변경되었고, 이를 클릭하면 Greeting을 정의할 수 있는 UI가 나타납니다. 해당 매개변수를 재정의 하여 빌드를 수행할 수 있습니다.
다중스텝을 위한 Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 02-04.MultiStep)
Pipeline에 다음과 같이 스크립트를 추가합니다.
pipeline {
+ agent any
+ stages {
+ stage('Build') {
+ steps {
+ sh 'echo "Hello World"'
+ sh '''
+ echo "Multiline shell steps works too"
+ ls -lah
+ '''
+ }
+ }
+ }
+}
+
'''
은 스크립트 정의 시 여러줄을 입력할 수 있도록 묶어주는 역할을 합니다. 해당 스크립트에서는 sh
로 구분된 스크립트 명령줄이 두번 수행됩니다.
실행되는 여러 스크립트의 수행을 stage
로 구분하기위해 기존 Pipeline 스크립트를 다음과 같이 수정합니다.
pipeline {
+ agent any
+ stages {
+ stage('Build-1') {
+ steps {
+ sh 'echo "Hello World"'
+ }
+ }
+ stage('Build-2') {
+ steps {
+ sh '''
+ echo "Multiline shell steps works too"
+ ls -lah
+ '''
+ }
+ }
+ }
+}
+
stage를 구분하였기 때문에 각 실행되는 sh
스크립트는 각 스테이지에서 한번씩 수행되며, 이는 빌드의 결과로 나타납니다.
Pipeline의 step을 추가하여 결과를 확인하는 과정을 설명합니다. 피보나치 수열을 수행하는 쉘 스크립트를 시간제한을 두어 수행하고 그 결과를 확인합니다.
',28),_={href:"https://namu.wiki/w/%ED%94%BC%EB%B3%B4%EB%82%98%EC%B9%98%20%EC%88%98%EC%97%B4",target:"_blank",rel:"noopener noreferrer"},y={href:"https://namu.wiki/w/%ED%94%BC%EB%B3%B4%EB%82%98%EC%B9%98",target:"_blank",rel:"noopener noreferrer"},P=t(`$ mkdir -p /var/jenkins_home/scripts
+$ cd /var/jenkins_home/scripts
+$ vi ./fibonacci.sh
+#!/bin/bash
+N=\${1:-10}
+
+a=0
+b=1
+
+echo "The Fibonacci series is : "
+
+for (( i=0; i<N; i++ ))
+do
+ echo "$a"
+ sleep 2
+ fn=$((a + b))
+ a=$b
+ b=$fn
+done
+# End of for loop
+
+$ chown -R jenkins /var/jenkins_home/
+$ chmod +x /var/jenkins_home/scripts/fibonacci.sh
+
다중스텝을 위한 Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 02-05.AddingStep)
Pipeline에 다음과 같이 스크립트를 추가합니다.
pipeline {
+ agent any
+ stages {
+ stage('Deploy') {
+ steps {
+ timeout(time: 1, unit: 'MINUTES') {
+ sh '/var/jenkins_home/scripts/fibonacci.sh 5'
+ }
+ timeout(time: 1, unit: 'MINUTES') {
+ sh '/var/jenkins_home/scripts/fibonacci.sh 32'
+ }
+ }
+ }
+ }
+}
+
steps
에 스크립트를 timeout
이 감싸고 있으며, 각 스크립트의 제한시간은 1분입니다. 빌드를 수행하면 최종적으로는 aborted
, 즉 중단됨 상태가 되는데 그 이유는 빌드 기록에서 해당 빌드를 클릭하면 확인 가능합니다.
Build History
에서 최신 빌드를 클릭합니다.
좌측 Pipeline Steps
를 클릭하면 Pipeline 수행 스텝을 확인할 수 있습니다.
첫번째로 나타나는 /var/jenkins_home/scripts/fibonacci.sh 5
를 수행하는 Shell Script
의 콘솔창 버튼을 클릭하면 잘 수행되었음을 확인 할 수 있습니다.
두번째로 나타나는 /var/jenkins_home/scripts/fibonacci.sh 32
를 수행하는 Shell Script
의 콘솔창 버튼을 클릭하면 다음과 같이 중도에 프로세스를 중지한 것을 확인 할 수 있습니다.
+ /var/jenkins_home/scripts/fibonacci.sh 32
+The Fibonacci series is :
+0
+1
+1
+2
+3
+...
+317811
+514229
+Sending interrupt signal to process
+/var/jenkins_home/scripts/fibonacci.sh: line 16: 13543 Terminated sleep 2
+832040
+/var/lib/jenkins/workspace/02-05.AddingStep@tmp/durable-e44bb232/script.sh: line 1: 13109 Terminated /var/jenkins_home/scripts/fibonacci.sh 32
+script returned exit code 143
+
프로젝트는 Job의 일부 입니다. 즉, 모든 프로젝트가 Job이지만 모든 Job이 프로젝트는 아닙니다. Job의 구조는 다음과 같습니다.
\\nFreeStyleProejct, MatrixProject, ExternalJob만 New job
에 표시됩니다.
Step 1에서는 stage
없이 기본 Pipeline을 실행하여 수행 테스트를 합니다.
Jenkins 로그인
\\n좌측 새로운 Item
클릭
Enter an item name
에 Job 이름 설정 (e.g. 2.Jobs)
Pipeline
선택 후 OK
버튼 클릭
Pipeline
항목 오른 쪽 Try sample Pipelie...
클릭하여 Hello world
클릭 후 저장
node {\\n echo 'Hello World'\\n}\\n
좌측 Build now
클릭
좌측 Build History
의 최근 빌드된 항목(e.g. #1) 우측에 마우스를 가져가면 dropdown 버튼이 생깁니다. 해당 버튼을 클릭하여 Console Output
클릭
수행된 echo
동작 출력을 확인합니다.
Started by user GyuSeok.Lee\\nRunning in Durability level: MAX_SURVIVABILITY\\n[Pipeline] Start of Pipeline\\n[Pipeline] node\\nRunning on Jenkins in /var/lib/jenkins/workspace/2.Jobs\\n[Pipeline] {\\n[Pipeline] echo\\nHello World\\n[Pipeline] }\\n[Pipeline] // node\\n[Pipeline] End of Pipeline\\nFinished: SUCCESS\\n
지난 1주차 스터디에이어 2주차 스터디를 진행하였습니다. 이번 스터디에서는 "쿠버네티스 네트워크" 및 "쿠버네티스 스토리지"를 중심으로 학습하였습니다.
참고 :
원활한 실습을 위해 인스턴스 타입을 변경한 후 진행합니다.
kops get ig
+NAME ROLE MACHINETYPE MIN MAX ZONES
+master-ap-northeast-2a Master t3.medium 1 1 ap-northeast-2a
+nodes-ap-northeast-2a Node t3.medium 1 1 ap-northeast-2a
+nodes-ap-northeast-2c Node t3.medium 1 1 ap-northeast-2c
+
kops edit ig master-ap-northeast-2a
+
+# 예제화면
+apiVersion: kops.k8s.io/v1alpha2
+kind: InstanceGroup
+metadata:
+ creationTimestamp: "2023-03-05T13:37:26Z"
+ labels:
+ kops.k8s.io/cluster: pkos.hyungwook.link
+ name: master-ap-northeast-2a
+spec:
+ image: 099720109477/ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-20230112
+ instanceMetadata:
+ httpPutResponseHopLimit: 3
+ httpTokens: required
+ machineType: t3.medium #기존 t3.medium에서 c5d.large로 변경
+ maxSize: 1
+ minSize: 1
+ role: Master
+ subnets:
+ - ap-northeast-2a
+
kops get ig
+NAME ROLE MACHINETYPE MIN MAX ZONES
+master-ap-northeast-2a Master c5d.large 1 1 ap-northeast-2a
+nodes-ap-northeast-2a Node c5d.large 1 1 ap-northeast-2a
+nodes-ap-northeast-2c Node c5d.large 1 1 ap-northeast-2c
+
kops update cluster --name pkos.hyungwook.link --yes
+
+kops rolling-update cluster --yes
+
externalTrafficPolicy
: ClusterIP ⇒ 2번 분산 및 SNAT으로 Client IP 확인 불가능 ← LoadBalancer
타입 (기본 모드) 동작externalTrafficPolicy
: Local ⇒ 1번 분산 및 ClientIP 유지, 워커 노드의 iptables 사용함반드시 AWS LoadBalancer 컨트롤러 파드 및 정책 설정이 필요함!
Proxy Protocol v2 비활성화
⇒ NLB에서 바로 파드로 인입, 단 ClientIP가 NLB로 SNAT 되어 Client IP 확인 불가능Proxy Protocol v2 활성화
⇒ NLB에서 바로 파드로 인입 및 ClientIP 확인 가능(→ 단 PPv2 를 애플리케이션이 인지할 수 있게 설정 필요)# 작업용 EC2 - 디플로이먼트 & 서비스 생성
+cat ~/pkos/2/echo-service-nlb.yaml | yh
+kubectl apply -f ~/pkos/2/echo-service-nlb.yaml
+
+# 확인
+kubectl get deploy,pod
+kubectl get svc,ep,ingressclassparams,targetgroupbindings
+
+NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
+service/kubernetes ClusterIP 100.64.0.1 <none> 443/TCP 7d
+service/svc-nlb-ip-type LoadBalancer 100.64.191.200 k8s-default-svcnlbip-bfcad9371a-250be02681485d95.elb.ap-northeast-2.amazonaws.com 80:31206/TCP 97s
+
+NAME ENDPOINTS AGE
+endpoints/kubernetes 172.30.37.41:443 7d
+endpoints/svc-nlb-ip-type 172.30.55.31:8080,172.30.71.86:8080 97s
+
+NAME GROUP-NAME SCHEME IP-ADDRESS-TYPE AGE
+ingressclassparams.elbv2.k8s.aws/alb 122m
+
+NAME SERVICE-NAME SERVICE-PORT TARGET-TYPE AGE
+targetgroupbinding.elbv2.k8s.aws/k8s-default-svcnlbip-c54bafee9a svc-nlb-ip-type 80 ip 95s
+
+kubectl get targetgroupbindings -o json | jq
+
k get pods -o wide
+NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
+deploy-echo-(생략) 1/1 Running 0 7m50s 172.30.55.31 i-089062ff9f50789ee <none> <none>
+deploy-echo-(생략) 1/1 Running 0 7m50s 172.30.71.86 i-096a645be0dd932b6 <none> <none>
+
사전 준비 :
공인도메인 소유, AWS Route53 도메인등록 상태, NLB 가 위치한 리전(서울)의 인증서 요청/발급 완료상태, ExternalDNS 준비완료 상태
# 사용 리전의 인증서 ARN 확인
+aws acm list-certificates
+aws acm list-certificates --max-items 10
+aws acm list-certificates --query 'CertificateSummaryList[].CertificateArn[]' --output text
+CERT_ARN=\`aws acm list-certificates --query 'CertificateSummaryList[].CertificateArn[]' --output text\`
+echo $CERT_ARN
+
# 자신의 도메인 변수 지정
+MyDomain=<자신의 도메인>
+MyDomain=websrv.$KOPS_CLUSTER_NAME
+
cat <<EOF | kubectl create -f -
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: deploy-echo
+spec:
+ replicas: 2
+ selector:
+ matchLabels:
+ app: deploy-websrv
+ template:
+ metadata:
+ labels:
+ app: deploy-websrv
+ spec:
+ terminationGracePeriodSeconds: 0
+ containers:
+ - name: akos-websrv
+ image: k8s.gcr.io/echoserver:1.5
+ ports:
+ - containerPort: 8080
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: svc-nlb-ip-type
+ annotations:
+ external-dns.alpha.kubernetes.io/hostname: "\${MyDomain}"
+ service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
+ service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
+ service.beta.kubernetes.io/aws-load-balancer-healthcheck-port: "8080"
+ service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
+ service.beta.kubernetes.io/aws-load-balancer-ssl-ports: "https"
+ service.beta.kubernetes.io/aws-load-balancer-ssl-cert: ${CERT_ARN}
+ service.beta.kubernetes.io/aws-load-balancer-backend-protocol: "http"
+spec:
+ ports:
+ - port: 80
+ targetPort: 8080
+ protocol: TCP
+ name: http
+ - port: 443
+ targetPort: 8080
+ protocol: TCP
+ name: https
+ type: LoadBalancer
+ loadBalancerClass: service.k8s.aws/nlb
+ selector:
+ app: deploy-websrv
+EOF
+
kubectl describe svc svc-nlb-ip-type | grep Annotations: -A8
+
+Annotations: external-dns.alpha.kubernetes.io/hostname: websrv.pkos.hyungwook.link
+ service.beta.kubernetes.io/aws-load-balancer-backend-protocol: http
+ service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: true
+ service.beta.kubernetes.io/aws-load-balancer-healthcheck-port: 8080
+ service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
+ service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
+ service.beta.kubernetes.io/aws-load-balancer-ssl-cert:
+ arn:aws:acm:ap-northeast-2:856117747411:certificate/208e809e-9ebf-4bb5-92c2-795868429e88
+ service.beta.kubernetes.io/aws-load-balancer-ssl-ports: https
+
insecure
옵션 없이 정상적으로 curl 응답 하는 것을 확인할 수 있습니다.curl -s http://websrv.pkos.hyungwook.link | grep Hostname
+Hostname: deploy-echo-5c4856dfd6-267pf
+
+curl -s https://websrv.pkos.hyungwook.link | grep Hostname
+Hostname: deploy-echo-5c4856dfd6-k9277
+
클러스터 내부의 서비스(ClusterIP, NodePort, Loadbalancer)를 외부로 노출(HTTP/HTTPS) - Web Proxy 역할
# EC2 instance profiles 에 IAM Policy 추가(attach)
+aws iam attach-role-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy --role-name masters.$KOPS_CLUSTER_NAME
+aws iam attach-role-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy --role-name nodes.$KOPS_CLUSTER_NAME
+
+# EC2 instance profiles 에 IAM Policy 추가(attach)
+aws iam attach-role-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy --role-name masters.$KOPS_CLUSTER_NAME
+aws iam attach-role-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy --role-name nodes.$KOPS_CLUSTER_NAME
+
# kOps 클러스터 편집 : 아래 내용 추가
+kops edit cluster
+-----
+spec:
+ certManager:
+ enabled: true
+ awsLoadBalancerController:
+ enabled: true
+ externalDns:
+ provider: external-dns
+
# 업데이트 적용
+kops update cluster --yes && echo && sleep 3 && kops rolling-update cluster
+
# 게임 파드와 Service, Ingress 배포
+kubectl apply -f ~/pkos/3/ingress1.yaml
+
kubectl get targetgroupbindings -n game-2048
+NAME SERVICE-NAME SERVICE-PORT TARGET-TYPE AGE
+k8s-game2048-service2-e48050abac service-2048 80 ip 87s
+
kubectl describe ingress -n game-2048 ingress-2048
+
+Name: ingress-2048
+Labels: <none>
+Namespace: game-2048
+Address: k8s-game2048-ingress2-fdfe8009a9-1424012699.ap-northeast-2.elb.amazonaws.com
+Ingress Class: alb
+Default backend: <default>
+Rules:
+ Host Path Backends
+ ---- ---- --------
+ *
+ / service-2048:80 (172.30.44.132:80,172.30.65.100:80)
+Annotations: alb.ingress.kubernetes.io/scheme: internet-facing
+ alb.ingress.kubernetes.io/target-type: ip
+Events:
+ Type Reason Age From Message
+ ---- ------ ---- ---- -------
+ Normal SuccessfullyReconciled 8m56s ingress Successfully reconciled
+
# 게임 접속 : ALB 주소로 웹 접속
+kubectl get ingress -n game-2048 ingress-2048 -o jsonpath="{.status.loadBalancer.ingress[0].hostname}" | awk '{ print "Game URL = http://"$1 }'
+Game URL = http://k8s-game2048-ingress2-fdfe8009a9-1424012699.ap-northeast-2.elb.amazonaws.com
+
kubectl delete ingress ingress-2048 -n game-2048
+kubectl delete svc service-2048 -n game-2048 && kubectl delete deploy deployment-2048 -n game-2048 && kubectl delete ns game-2048
+
# 인스턴스 스토어 볼륨이 있는 c5 모든 타입의 스토리지 크기
+aws ec2 describe-instance-types \\
+ --filters "Name=instance-type,Values=c5*" "Name=instance-storage-supported,Values=true" \\
+ --query "InstanceTypes[].[InstanceType, InstanceStorageInfo.TotalSizeInGB]" \\
+ --output table
+--------------------------
+| DescribeInstanceTypes |
++---------------+--------+
+| c5d.large | 50 |
+| c5d.12xlarge | 1800 |
+...
+
+# 워커 노드 Public IP 확인
+aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value}" --filters Name=instance-state-name,Values=running --output table
+
+-------------------------------------------------------------------------
+| DescribeInstances |
++------------------------------------------------------+----------------+
+| InstanceName | PublicIPAdd |
++------------------------------------------------------+----------------+
+| nodes-ap-northeast-2c.pkos.hyungwook.link | 13.209.75.228 |
+| master-ap-northeast-2a.masters.pkos.hyungwook.link | 3.38.117.78 |
+| nodes-ap-northeast-2a.pkos.hyungwook.link | 52.79.61.228 |
++------------------------------------------------------+----------------+
+
+# 워커 노드 Public IP 변수 지정
+W1PIP=<워커 노드 1 Public IP>
+W2PIP=<워커 노드 2 Public IP>
+W1PIP=13.209.75.228
+W2PIP=52.79.61.228
+echo "export W1PIP=$W1PIP" >> /etc/profile
+echo "export W2PIP=$W2PIP" >> /etc/profile
+
ssh -i ~/.ssh/id_rsa ubuntu@$W1PIP sudo apt install -y nvme-cli
+ssh -i ~/.ssh/id_rsa ubuntu@$W2PIP sudo apt install -y nvme-cli
+ssh -i ~/.ssh/id_rsa ubuntu@$W1PIP sudo nvme list
+ssh -i ~/.ssh/id_rsa ubuntu@$W2PIP sudo nvme list
+
# 워커 노드 스토리지 확인 : NVMe SSD 인스턴스 스토어 볼륨 확인
+ssh -i ~/.ssh/id_rsa ubuntu@$W1PIP lsblk -e 7
+ssh -i ~/.ssh/id_rsa ubuntu@$W2PIP lsblk -e 7
+NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
+nvme1n1 259:0 0 46.6G 0 disk
+nvme0n1 259:1 0 128G 0 disk
+├─nvme0n1p1 259:2 0 127.9G 0 part /
+├─nvme0n1p14 259:3 0 4M 0 part
+└─nvme0n1p15 259:4 0 106M 0 part /boot/efi
+
+ssh -i ~/.ssh/id_rsa ubuntu@$W1PIP df -hT -t ext4
+ssh -i ~/.ssh/id_rsa ubuntu@$W2PIP df -hT -t ext4
+Filesystem Type Size Used Avail Use% Mounted on
+/dev/root ext4 124G 4.2G 120G 4% /
+
+ssh -i ~/.ssh/id_rsa ubuntu@$W1PIP lspci | grep Non-Volatile
+00:04.0 Non-Volatile memory controller: Amazon.com, Inc. Device 8061
+00:1f.0 Non-Volatile memory controller: Amazon.com, Inc. NVMe SSD Controller
+
+# 파일시스템 생성
+ssh -i ~/.ssh/id_rsa ubuntu@$W1PIP sudo mkfs -t xfs /dev/nvme1n1
+ssh -i ~/.ssh/id_rsa ubuntu@$W2PIP sudo mkfs -t xfs /dev/nvme1n1
+
+# /data 디렉토리 생성
+ssh -i ~/.ssh/id_rsa ubuntu@$W1PIP sudo mkdir /data
+ssh -i ~/.ssh/id_rsa ubuntu@$W2PIP sudo mkdir /data
+
+# /data 마운트
+ssh -i ~/.ssh/id_rsa ubuntu@$W1PIP sudo mount /dev/nvme1n1 /data
+ssh -i ~/.ssh/id_rsa ubuntu@$W2PIP sudo mount /dev/nvme1n1 /data
+
+# 파일시스템 포맷 및 마운트 확인
+ssh -i ~/.ssh/id_rsa ubuntu@$W1PIP df -hT -t ext4 -t xfs
+ssh -i ~/.ssh/id_rsa ubuntu@$W2PIP df -hT -t ext4 -t xfs
+Filesystem Type Size Used Avail Use% Mounted on
+/dev/root ext4 124G 4.2G 120G 4% /
+/dev/nvme1n1 xfs 47G 365M 47G 1% /data
+
# 마스터노드의 이름 확인해두기
+kubectl get node | grep control-plane | awk '{print $1}'
+i-066cdb714fc6545c0
+
+# 배포 : vim 직접 편집할것
+curl -s -O https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.23/deploy/local-path-storage.yaml
+vim local-path-storage.yaml
+----------------------------
+# 아래 빨간 부분은 추가 및 수정
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: local-path-provisioner
+ namespace: local-path-storage
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: local-path-provisioner
+ template:
+ metadata:
+ labels:
+ app: local-path-provisioner
+ spec:
+ nodeSelector:
+ kubernetes.io/hostname: "<각자 자신의 마스터 노드 이름 입력>"
+ tolerations:
+ - effect: NoSchedule
+ key: node-role.kubernetes.io/control-plane
+ operator: Exists
+...
+kind: ConfigMap
+apiVersion: v1
+metadata:
+ name: local-path-config
+ namespace: local-path-storage
+data:
+ config.json: |-
+ {
+ "nodePathMap":[
+ {
+ "node":"DEFAULT_PATH_FOR_NON_LISTED_NODES",
+ "paths":["/data/local-path"]
+ }
+ ]
+ }
+----------------------------
+
+# 배포
+kubectl apply -f local-path-storage.yaml
+
+# 확인 : 마스터노드에 배포됨
+kubectl get-all -n local-path-storage
+NAME NAMESPACE AGE
+configmap/kube-root-ca.crt local-path-storage 12s
+configmap/local-path-config local-path-storage 12s
+pod/local-path-provisioner-6bff65dcd8-vgwfk local-path-storage 12s
+serviceaccount/default local-path-storage 12s
+serviceaccount/local-path-provisioner-service-account local-path-storage 12s
+deployment.apps/local-path-provisioner local-path-storage 12s
+replicaset.apps/local-path-provisioner-6bff65dcd8 local-path-storage 12s
+
+kubectl get pod -n local-path-storage -owide
+NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
+local-path-provisioner-6bff65dcd8-vgwfk 1/1 Running 0 17s 172.30.63.103 i-072786762169226a7 <none> <none>
+
+kubectl describe cm -n local-path-storage local-path-config
+
+kubectl get sc local-path
+NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
+local-path rancher.io/local-path Delete WaitForFirstConsumer false 34s
+
# PVC 생성
+kubectl apply -f ~/pkos/3/localpath1.yaml
+
+# PVC 확인 : 아직 Pod Boud가 되지 않았으므로 Pending
+kubectl describe pvc
+kubectl get pvc
+
+NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
+localpath-claim Pending local-path 58s
+
+# 파드 생성
+kubectl apply -f ~/pkos/3/localpath2.yaml
+
+# 파드확인 : 정상적으로 Bound된 것으로 확인
+kubectl get pod,pv,pvc
+
+NAME READY STATUS RESTARTS AGE
+pod/app-localpath 1/1 Running 0 56s
+
+NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
+persistentvolume/pvc-37743f20-e30d-491c-b11c-5e5b7d33a476 1Gi RWO Delete Bound default/localpath-claim local-path 49s
+
+NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
+persistentvolumeclaim/localpath-claim Bound pvc-37743f20-e30d-491c-b11c-5e5b7d33a476 1Gi RWO local-path 3m57s
+
# 파드 데이터 확인 : app-localpath 파드에서 데이터 쌓이는 것 확인
+kubectl exec -it app-localpath -- tail -f /data/out.txt
+Sun Jan 29 05:13:45 UTC 2023
+
+ssh -i ~/.ssh/id_rsa ubuntu@$W2PIP tree /data
+/data
+0 directories, 0 files
+
+ssh -i ~/.ssh/id_rsa ubuntu@$W1PIP tree /data
+/data
+└── local-path
+ └── pvc-37743f20-e30d-491c-b11c-5e5b7d33a476_default_localpath-claim
+ └── out.txt
+
+2 directories, 1 file
+
+# 노드 데이터 확인 : app-localpath 파드가 배포된 노드에 접속 후 데이터 쌓이는 것 확인
+ssh -i ~/.ssh/id_rsa ubuntu@$W2PIP tail -f /data/local-path/pvc-ce742b90-755a-4b52-9693-595cbf55dfb0_default_localpath-claim/out.txt
+Sun Jan 29 05:13:45 UTC 2023
+...
+
LocalPath 성능측정은 추후 별도 정리 후 업로드 예정
# EBS Driver 확인 Kops 설치 시 기본 배포
+kubectl get pod -n kube-system -l app.kubernetes.io/instance=aws-ebs-csi-driver
+
+NAME READY STATUS RESTARTS AGE
+ebs-csi-controller-6d8fd64c78-q5qfn 5/5 Running 0 5d23h
+ebs-csi-node-9cfss 3/3 Running 0 5d23h
+ebs-csi-node-crhbx 3/3 Running 0 5d23h
+ebs-csi-node-zbjgj 3/3 Running 0 5d23h
+
+# 스토리지 클래스 확인
+kubectl get sc kops-csi-1-21 kops-ssd-1-17
+
+kubectl describe sc kops-csi-1-21 | grep Parameters
+Parameters: encrypted=true,type=gp3
+kubectl describe sc kops-ssd-1-17 | grep Parameters
+Parameters: encrypted=true,type=gp2
+
+
# PVC 생성
+kubectl apply -f ~/pkos/3/awsebs-pvc.yaml
+
+# 파드 생성
+kubectl apply -f ~/pkos/3/awsebs-pod.yaml
+
+# PVC, 파드 확인
+kubectl get pvc,pv,pod
+NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
+persistentvolumeclaim/ebs-claim Bound pvc-fb5b5e1c-76ef-4a43-9b94-9af2b1b1e5f7 4Gi RWO kops-csi-1-21 41m
+
+NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
+persistentvolume/pvc-fb5b5e1c-76ef-4a43-9b94-9af2b1b1e5f7 4Gi RWO Delete Bound default/ebs-claim kops-csi-1-21 40m
+
+NAME READY STATUS RESTARTS AGE
+pod/app 1/1 Running 0 3m15s
+
+# 실제 쌓이는 데이터 확인
+kubectl exec app -- tail -f /data/out.txt
+
+Sat Mar 18 14:49:25 UTC 2023
+Sat Mar 18 14:49:30 UTC 2023
+Sat Mar 18 14:49:35 UTC 2023
+...
+
+# 파드 내에서 볼륨 정보 확인
+kubectl exec -it app -- sh -c 'df -hT --type=ext4'
+Filesystem Type Size Used Avail Use% Mounted on
+/dev/nvme1n1 ext4 3.9G 16M 3.8G 1% /data
+/dev/root ext4 124G 4.9G 120G 4% /etc/hosts
+
+# 추가된 EBS 볼륨 상세 정보 확인
+while true; do aws ec2 describe-volumes --filters Name=tag:ebs.csi.aws.com/cluster,Values=true --query "Volumes[].{VolumeId: VolumeId, VolumeType: VolumeType, InstanceId: Attachments[0].InstanceId, State: Attachments[0].State}" --output text; date; sleep 1; done
+
+(중략)
+i-078613f7b7cd9e352 attached vol-071ebb777dc2ac3cd gp3 # 시간이 지난 후 추가되는 것 확인
+
# 현재 pv 의 이름을 기준하여 4G > 10G 로 증가 : .spec.resources.requests.storage의 4Gi 를 10Gi로 변경
+kubectl get pvc ebs-claim -o jsonpath={.spec.resources.requests.storage} ; echo
+kubectl get pvc ebs-claim -o jsonpath={.status.capacity.storage} ; echo
+kubectl patch pvc ebs-claim -p '{"spec":{"resources":{"requests":{"storage":"10Gi"}}}}'
+
+# 확인 : 볼륨 용량 수정 반영이 되어야 되니, 수치 반영이 조금 느릴수 있다
+kubectl exec -it app -- sh -c 'df -hT --type=ext4'
+kubectl df-pv
+aws ec2 describe-volumes --volume-ids $(kubectl get pv -o jsonpath="{.items[0].spec.csi.volumeHandle}") | jq
+
kubectl delete pod app & kubectl delete pvc ebs-claim
+
이번 글에서는 Kops 환경에서의 네트워크와 스토리지를 활용하는 방법을 정리해보았습니다.
일반적인 K8s와 달리 AWS 환경에서 EKS, Kops 등을 활용하게 된다면, AWS 리소스와의 연계를 통해 관리의 효율성과 편의성을 체감할 수 있었습니다.
다만, K8s만 알고 AWS는 잘 모르거나 또 그 반대의 상황에는 진입장벽이 있을 수 있겠다는 생각이 들었네요. 물론 이러한 부분만 해소된다면 관리형 쿠버네티스를 200% 활용할 수 있을 것 같습니다.
다음시간에는 GitOps와 관련된 주제로 찾아올 예정입니다.
`,15);function f(y,P){const a=i("ExternalLinkIcon");return p(),c("div",null,[r,n("p",null,[u,s(" 의 EC2 인스턴스 스토어(임시 블록 스토리지) 설정 작업 - "),n("a",d,[s("링크"),e(a)]),s(" , NVMe SSD - "),n("a",v,[s("링크"),e(a)])]),m,n("ul",null,[n("li",null,[s("호스트 Path 를 사용하는 PV/PVC : local-path-provisioner 스트리지 클래스 배포 - "),n("a",b,[s("링크"),e(a)])])]),k,n("p",null,[s("Volume (ebs-csi-controller) : EBS CSI driver 동작 : 볼륨 생성 및 파드에 볼륨 연결 - "),n("a",h,[s("링크"),e(a)])]),g])}const S=l(o,[["render",f],["__file","02-kops-network-storage.html.vue"]]),E=JSON.parse('{"path":"/02-PrivatePlatform/Kubernetes/05-Kops/02-kops-network-storage.html","title":"[PKOS] 2편 - 네트워크 & 스토리지","lang":"ko-KR","frontmatter":{"description":"AWS Kops 설치 및 기본 사용","tag":["Kubernetes","Kops","EKS","PKOS"],"head":[["meta",{"property":"og:url","content":"https://docmoa.github.io/02-PrivatePlatform/Kubernetes/05-Kops/02-kops-network-storage.html"}],["meta",{"property":"og:site_name","content":"docmoa"}],["meta",{"property":"og:title","content":"[PKOS] 2편 - 네트워크 & 스토리지"}],["meta",{"property":"og:description","content":"AWS Kops 설치 및 기본 사용"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://raw.githubusercontent.com/hyungwook0221/img/main/uPic/IMeYr3.jpg"}],["meta",{"property":"og:locale","content":"ko-KR"}],["meta",{"property":"og:updated_time","content":"2023-09-18T13:12:54.000Z"}],["meta",{"name":"twitter:card","content":"summary_large_image"}],["meta",{"name":"twitter:image:alt","content":"[PKOS] 2편 - 네트워크 & 스토리지"}],["meta",{"property":"article:tag","content":"Kubernetes"}],["meta",{"property":"article:tag","content":"Kops"}],["meta",{"property":"article:tag","content":"EKS"}],["meta",{"property":"article:tag","content":"PKOS"}],["meta",{"property":"article:modified_time","content":"2023-09-18T13:12:54.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"[PKOS] 2편 - 네트워크 & 스토리지\\",\\"image\\":[\\"https://raw.githubusercontent.com/hyungwook0221/img/main/uPic/IMeYr3.jpg\\",\\"https://raw.githubusercontent.com/hyungwook0221/img/main/uPic/rV5KOK.jpg\\",\\"https://raw.githubusercontent.com/hyungwook0221/img/main/uPic/rZx1tE.jpg\\",\\"https://raw.githubusercontent.com/hyungwook0221/img/main/uPic/kQcEaN.jpg\\",\\"https://raw.githubusercontent.com/hyungwook0221/img/main/uPic/uVSCZA.jpg\\",\\"https://raw.githubusercontent.com/hyungwook0221/img/main/uPic/OPTdsH.jpg\\",\\"https://raw.githubusercontent.com/hyungwook0221/img/main/uPic/u5cdKs.jpg\\",\\"https://raw.githubusercontent.com/hyungwook0221/img/main/uPic/KTZTUM.jpg\\",\\"https://docs.aws.amazon.com/ko_kr/AWSEC2/latest/UserGuide/images/instance_storage.png\\",\\"https://raw.githubusercontent.com/hyungwook0221/img/main/uPic/6iX9dg.jpg\\",\\"https://raw.githubusercontent.com/hyungwook0221/img/main/uPic/Y6CYPg.jpg\\",\\"https://raw.githubusercontent.com/hyungwook0221/img/main/uPic/dGge8O.jpg\\"],\\"dateModified\\":\\"2023-09-18T13:12:54.000Z\\",\\"author\\":[]}"]]},"headers":[{"level":2,"title":"0. 사전준비","slug":"_0-사전준비","link":"#_0-사전준비","children":[{"level":3,"title":"1) Kops 클러스터의 인스턴 그룹 변경","slug":"_1-kops-클러스터의-인스턴-그룹-변경","link":"#_1-kops-클러스터의-인스턴-그룹-변경","children":[]}]},{"level":2,"title":"2. NLB","slug":"_2-nlb","link":"#_2-nlb","children":[{"level":3,"title":"1) NLB Mode 정리","slug":"_1-nlb-mode-정리","link":"#_1-nlb-mode-정리","children":[]},{"level":3,"title":"2) 서비스 배포","slug":"_2-서비스-배포","link":"#_2-서비스-배포","children":[]},{"level":3,"title":"3) 접속확인","slug":"_3-접속확인","link":"#_3-접속확인","children":[]}]},{"level":2,"title":"3. NLB에 TLS 적용하기(feat. ACM)","slug":"_3-nlb에-tls-적용하기-feat-acm","link":"#_3-nlb에-tls-적용하기-feat-acm","children":[{"level":3,"title":"1) 환경구성","slug":"_1-환경구성","link":"#_1-환경구성","children":[]},{"level":3,"title":"2) 샘플 애플리케이션 배포","slug":"_2-샘플-애플리케이션-배포","link":"#_2-샘플-애플리케이션-배포","children":[]},{"level":3,"title":"3) 인증서 적용 확인","slug":"_3-인증서-적용-확인","link":"#_3-인증서-적용-확인","children":[]}]},{"level":2,"title":"3. Ingress","slug":"_3-ingress","link":"#_3-ingress","children":[{"level":3,"title":"1) 환경구성","slug":"_1-환경구성-1","link":"#_1-환경구성-1","children":[]},{"level":3,"title":"2) 서비스/파드 배포 테스트 with Ingress(ALB)\\\\","slug":"_2-서비스-파드-배포-테스트-with-ingress-alb","link":"#_2-서비스-파드-배포-테스트-with-ingress-alb","children":[]}]},{"level":2,"title":"4. 쿠버네티스 스토리지 : EBS","slug":"_4-쿠버네티스-스토리지-ebs","link":"#_4-쿠버네티스-스토리지-ebs","children":[{"level":3,"title":"1) 환경구성","slug":"_1-환경구성-2","link":"#_1-환경구성-2","children":[]},{"level":3,"title":"2) HostPath 실습","slug":"_2-hostpath-실습","link":"#_2-hostpath-실습","children":[]},{"level":3,"title":"3) AWS EBS Controller","slug":"_3-aws-ebs-controller","link":"#_3-aws-ebs-controller","children":[]}]},{"level":2,"title":"5. 마무리","slug":"_5-마무리","link":"#_5-마무리","children":[]}],"git":{"createdTime":1695042774000,"updatedTime":1695042774000,"contributors":[{"name":"Great-Stone","email":"hahohh@gmail.com","commits":1}]},"readingTime":{"minutes":7.23,"words":2169},"filePathRelative":"02-PrivatePlatform/Kubernetes/05-Kops/02-kops-network-storage.md","localizedDate":"2023년 9월 18일","excerpt":"\\n지난 1주차 스터디에이어 2주차 스터디를 진행하였습니다. 이번 스터디에서는 \\"쿠버네티스 네트워크\\" 및 \\"쿠버네티스 스토리지\\"를 중심으로 학습하였습니다.
\\n\\n\\n참고 :
\\n
\\n원활한 실습을 위해 인스턴스 타입을 변경한 후 진행합니다.
kops get ig\\nNAME\\t\\t\\tROLE\\tMACHINETYPE\\tMIN\\tMAX\\tZONES\\nmaster-ap-northeast-2a\\tMaster\\tt3.medium\\t1\\t1\\tap-northeast-2a\\nnodes-ap-northeast-2a\\tNode\\tt3.medium\\t1\\t1\\tap-northeast-2a\\nnodes-ap-northeast-2c\\tNode\\tt3.medium\\t1\\t1\\tap-northeast-2c\\n
기본적으로 Terraform 오픈소스는 커맨드라인 도구입니다.
Terraform 명령은 수동으로 입력하거나 스크립트에서 자동으로 실행됩니다.
명령은 Linux, Windows 또는 MacOS에 상관없이 동일합니다.
Terraform에는 다른 작업을 수행하는 하위 명령들이 있습니다.
# Basic Terraform Commands
+terraform version
+terraform help
+terraform init
+terraform plan
+terraform apply
+terraform destroy
+
특정 하위 명령에 대한 도움말을 보려면 terraform <subcommand> help
를 입력합니다.
resource "ncloud_vpc" "vpc" {
+ ipv4_cidr_block = "10.0.0.0/16"
+}
+
Terraform 코드는 모든 클라우드 또는 플랫폼에서 인프라를 프로비저닝하기 위해 특별히 설계된 선언적 언어입니다.
줄 주석은 *
(octothorpe, 별표) 또는 #
(파운드) 기호로 시작합니다....샵! #
# This is a line comment.
+
블록 주석은 /*
와 */
기호 사이에 포함됩니다.
/* This is a block comment.
+Block comments can span multiple lines.
+The comment ends with this symbol: */
+
Workspace는 Terraform 코드가 포함 된 폴더 또는 디렉토리입니다.
Terraform 파일은 항상* .tf 또는* .tfvars 확장자로 끝납니다. 실행 시 해당 파일들은 하나로 동작합니다.
대부분의 Terraform Workspaces에는 일반적으로 아래 3개정도의 파일을 둡니다. (정해진건 아닙니다.)
`,10),x={href:"http://main.tf",target:"_blank",rel:"noopener noreferrer"},C={href:"http://variables.tf",target:"_blank",rel:"noopener noreferrer"},P={href:"http://outputs.tf",target:"_blank",rel:"noopener noreferrer"},q=a("h2",{id:"terraform-init",tabindex:"-1"},[a("a",{class:"header-anchor",href:"#terraform-init"},[a("span",null,"Terraform Init")])],-1),L=a("div",{class:"language-hcl","data-ext":"hcl","data-title":"hcl"},[a("pre",{hcl:"",class:"language-hcl"},[a("code",null,[n(`$ terraform init +Initializing the backend... + +Initializing provider plugins... +- Finding navercloudplatform/ncloud versions matching `),a("span",{class:"token string"},'">= 2.1.2"'),n(`... +- Installing navercloudplatform/ncloud v2.`),a("span",{class:"token number"},"1.2"),n(`... +- Installed navercloudplatform/ncloud v2.`),a("span",{class:"token number"},"1.2"),n(" (signed by a HashiCorp partner, key ID "),a("span",{class:"token number"},"9"),n(`DCE24305722E9C9) +... +Terraform has been successfully initialized! +`)])]),a("div",{class:"highlight-lines"},[a("div",{class:"highlight-line"}," "),a("br"),a("br"),a("br"),a("br"),a("br"),a("br"),a("br"),a("br")])],-1),W=a("p",null,[n("Terraform은 필요한 Provider(공급자)와 Module(모듈)을 가져와 "),a("code",null,".terraform"),n(" 디렉터리에 저장합니다. 모듈 또는 공급자를 추가, 변경 또는 업데이트하는 경우 init를 다시 실행해야합니다.")],-1),I=a("h2",{id:"terraform-plan",tabindex:"-1"},[a("a",{class:"header-anchor",href:"#terraform-plan"},[a("span",null,"Terraform Plan")])],-1),N=a("div",{class:"language-bash line-numbers-mode","data-ext":"sh","data-title":"sh"},[a("pre",{bash:"",class:"language-bash"},[a("code",null,[n(`$ terraform plan +`),a("span",{class:"token punctuation"},".."),n(`. +Terraform will perform the following actions: + + `),a("span",{class:"token comment"},"# ncloud_vpc.vpc will be created"),n(` + + resource `),a("span",{class:"token string"},'"ncloud_vpc"'),n(),a("span",{class:"token string"},'"vpc"'),n(),a("span",{class:"token punctuation"},"{"),n(` + + default_access_control_group_no `),a("span",{class:"token operator"},"="),n(),a("span",{class:"token punctuation"},"("),n("known after apply"),a("span",{class:"token punctuation"},")"),n(` + + default_network_acl_no `),a("span",{class:"token operator"},"="),n(),a("span",{class:"token punctuation"},"("),n("known after apply"),a("span",{class:"token punctuation"},")"),n(` + + default_private_route_table_no `),a("span",{class:"token operator"},"="),n(),a("span",{class:"token punctuation"},"("),n("known after apply"),a("span",{class:"token punctuation"},")"),n(` + + default_public_route_table_no `),a("span",{class:"token operator"},"="),n(),a("span",{class:"token punctuation"},"("),n("known after apply"),a("span",{class:"token punctuation"},")"),n(` + + `),a("span",{class:"token function"},"id"),n(" "),a("span",{class:"token operator"},"="),n(),a("span",{class:"token punctuation"},"("),n("known after apply"),a("span",{class:"token punctuation"},")"),n(` + + ipv4_cidr_block `),a("span",{class:"token operator"},"="),n(),a("span",{class:"token string"},'"10.0.0.0/16"'),n(` + + name `),a("span",{class:"token operator"},"="),n(),a("span",{class:"token punctuation"},"("),n("known after apply"),a("span",{class:"token punctuation"},")"),n(` + + vpc_no `),a("span",{class:"token operator"},"="),n(),a("span",{class:"token punctuation"},"("),n("known after apply"),a("span",{class:"token punctuation"},")"),n(` + `),a("span",{class:"token punctuation"},"}"),n(` + +Plan: `),a("span",{class:"token number"},"1"),n(" to add, "),a("span",{class:"token number"},"0"),n(" to change, "),a("span",{class:"token number"},"0"),n(` to destroy. +`)])]),a("div",{class:"highlight-lines"},[a("div",{class:"highlight-line"}," "),a("br"),a("br"),a("br"),a("br"),a("br"),a("br"),a("br"),a("br"),a("br"),a("br"),a("br"),a("br"),a("br"),a("br"),a("br"),a("br")]),a("div",{class:"line-numbers","aria-hidden":"true"},[a("div",{class:"line-number"}),a("div",{class:"line-number"}),a("div",{class:"line-number"}),a("div",{class:"line-number"}),a("div",{class:"line-number"}),a("div",{class:"line-number"}),a("div",{class:"line-number"}),a("div",{class:"line-number"}),a("div",{class:"line-number"}),a("div",{class:"line-number"}),a("div",{class:"line-number"}),a("div",{class:"line-number"}),a("div",{class:"line-number"}),a("div",{class:"line-number"}),a("div",{class:"line-number"}),a("div",{class:"line-number"}),a("div",{class:"line-number"})])],-1),B=t(`변경 사항을 적용하기 전에 terraform plan
으로 미리 구성의 변경을 살펴봅니다.
Terraform 변수는 variables.tf
라는 파일에 일반적으로 위치 시킵니다.(이름은 변경 가능) 변수는 기본 설정을 가질 수 있습니다. 기본값을 생략하면 사용자에게 값을 입력하라는 메시지가 표시됩니다. 여기서 우리는 사용하려는 변수를 선언 합니다.
variable "prefix" {
+ description = "This prefix will be included in the name of most resources."
+}
+
+variable "vpc_cidr" {
+ description = "A cidr option for instances into the VPC."
+ default = "10.0.0.0/16"
+}
+
일부 변수를 정의한 후에는 다른 방법으로 설정하고 재정의 할 수 있습니다. 다음은 각 방법의 우선 순위입니다.
이 목록은 가장 높은 우선 순위 (1)에서 가장 낮은 순위 (5)로 나타냅니다.
즉, CLI 실행시 -var
로 지정되는 Command line flag
가 가장 우선합니다.
실습을 위해 다음장으로 이동하세요.
`,11);function z(S,E){const r=o("ExternalLinkIcon"),s=o("RouteLink");return i(),c("div",null,[u,a("ul",null,[a("li",null,[h,a("ul",null,[a("li",null,[n("Terraform Github : "),a("a",f,[n("https://github.com/hashicorp/terraform"),e(r)])])])]),b,a("li",null,[k,a("ul",null,[a("li",null,[n("다운로드 : "),a("a",g,[n("https://www.terraform.io/downloads.html"),e(r)])])])])]),_,v,T,a("p",null,[n("Terraform 코드는 "),a("a",w,[n("HCL2 툴킷"),e(r)]),n("을 기반으로합니다. HCL은 HashiCorp Configuration Language를 나타냅니다.")]),y,a("ul",null,[a("li",null,[a("a",x,[n("main.tf"),e(r)]),n(" - 대부분의 기능 코드는 여기에 있습니다.")]),a("li",null,[a("a",C,[n("variables.tf"),e(r)]),n(" - 이 파일은 변수를 저장하기위한 것입니다.")]),a("li",null,[a("a",P,[n("outputs.tf"),e(r)]),n(" - 테라 폼 실행 후 표시되는 내용을 정의합니다.")])]),q,L,W,I,N,B,a("p",null,[e(s,{to:"/03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/02-z-lab_terraform_basic.html"},{default:p(()=>[n("💻 Lab - Setup and Basic Usage")]),_:1})])])}const D=l(d,[["render",z],["__file","02-terraform-basic.html.vue"]]),G=JSON.parse('{"path":"/03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/02-terraform-basic.html","title":"02. 테라폼 기본","lang":"ko-KR","frontmatter":{"description":"Naver Cloud Platform에서의 Terraform 실습","tag":["ncloud","ncp","terraform","workshop"],"head":[["meta",{"property":"og:url","content":"https://docmoa.github.io/03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/02-terraform-basic.html"}],["meta",{"property":"og:site_name","content":"docmoa"}],["meta",{"property":"og:title","content":"02. 테라폼 기본"}],["meta",{"property":"og:description","content":"Naver Cloud Platform에서의 Terraform 실습"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"ko-KR"}],["meta",{"property":"og:updated_time","content":"2023-09-18T13:12:54.000Z"}],["meta",{"property":"article:tag","content":"ncloud"}],["meta",{"property":"article:tag","content":"ncp"}],["meta",{"property":"article:tag","content":"terraform"}],["meta",{"property":"article:tag","content":"workshop"}],["meta",{"property":"article:modified_time","content":"2023-09-18T13:12:54.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"02. 테라폼 기본\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2023-09-18T13:12:54.000Z\\",\\"author\\":[]}"]]},"headers":[{"level":2,"title":"Terraform 이란?","slug":"terraform-이란","link":"#terraform-이란","children":[]},{"level":2,"title":"Terraform Command Line","slug":"terraform-command-line","link":"#terraform-command-line","children":[]},{"level":2,"title":"Terraform Help","slug":"terraform-help","link":"#terraform-help","children":[]},{"level":2,"title":"Terraform Code","slug":"terraform-code","link":"#terraform-code","children":[]},{"level":2,"title":"Terraform Comments","slug":"terraform-comments","link":"#terraform-comments","children":[]},{"level":2,"title":"Terraform Workspaces","slug":"terraform-workspaces","link":"#terraform-workspaces","children":[]},{"level":2,"title":"Terraform Init","slug":"terraform-init","link":"#terraform-init","children":[]},{"level":2,"title":"Terraform Plan","slug":"terraform-plan","link":"#terraform-plan","children":[]},{"level":2,"title":"변수는 어디에?","slug":"변수는-어디에","link":"#변수는-어디에","children":[]},{"level":2,"title":"변수에 값을 할당하는 방식과 우선순위","slug":"변수에-값을-할당하는-방식과-우선순위","link":"#변수에-값을-할당하는-방식과-우선순위","children":[]}],"git":{"createdTime":1695042774000,"updatedTime":1695042774000,"contributors":[{"name":"Great-Stone","email":"hahohh@gmail.com","commits":1}]},"readingTime":{"minutes":1.24,"words":372},"filePathRelative":"03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/02-terraform-basic.md","localizedDate":"2023년 9월 18일","excerpt":"\\nTerraform은 오픈 소스 프로비저닝 도구입니다.
\\nGo로 작성된 단일 바이너리로 제공됩니다. Terraform은 크로스 플랫폼이며 Linux, Windows 또는 MacOS에서 실행할 수 있습니다.
\\nterraform 설치는 쉽습니다. zip 파일을 다운로드하고 압축을 풀고 실행하기 만하면됩니다.
\\n@slidestart blood
@slideend
git 이 설치되어있는 경우 git clone
을 통해 코드를 받습니다.$ git clone https://github.com/ncp-hc/workshop-oss.git
Download만을 원하는 경우 아래 Download ZIP
을 선택합니다.
Open Folder...
를 클릭합니다.lab01
을 열어줍니다.@slidestart blood
@slideend
편집기가 준비가 되었으면 터미널을 열고 몇가지 기본적인 Terraform 명령을 수행합니다.
Linux/Mac의 경우 터미널
에서 수행하거나 Windows의 경우 명령 프롬프트
에서 실행하게 됩니다.
VSCode 편집기를 사용하게 되면, 편집기의 터미널(Terminal) 기능으로 함께 사용할 수 있습니다.
terraform version
+
terraform help
+
@slidestart blood
@slideend
NCP에 인증하고 리소스를 빌드하기 위해 Terraform은 적절한 자격 증명 세트를 제공하도록 요구합니다.
이 교육 환경을 위해 NCP의 자격 증명을 준비하여 환경 변수로 저장합니다. Terraform은 쉘 환경에 구성된 환경 변수를 자동으로 읽고 사용합니다.
NCLOUD_ACCESS_KEY
NCLOUD_SECRET_KEY
NCLOUD_REGION
를 환경변수로 등록합니다.위험
API 자격증명정보는 실수로 공개된 저장소에 노출되거나 복사되면 위헐합니다.
자격증명(API 인증키)를 코드에 저장하지 않는 것을 권장합니다.
@slidestart blood
*.tf
또는 *.tfvars
로 끝나는 모든 것을 읽습니다.main.tf
, variables.tf
, outputs.tf
파일로 구성됩니다.예를 들어 모든 로드 밸런서 구성 코드를 load_balancer.tf
에 구성하기
@slideend
코드 편집기의 파일 목록이 보이십니까?
Terraform 코드는 항상 .tf
확장자로 끝납니다. 원하는 만큼 Terraform 파일을 가질 수 있지만 일반적으로 다음 세 가지를 구성합니다.
main.tf
- 대부분의 Terraform 코드가 저장되는 위치입니다. 이것은 자원을 구축하는 부분입니다.variables.tf
- 이 파일을 사용하여 사용자가 사용할 수 있는 변수를 정의합니다.output.tf
- 이 파일에는 성공적인 Terraform 실행이 끝날 때 표시될 출력이 포함되어 있습니다.Terraform에서 *.tf
와 *.tfvars
로 끝나지 않는 파일은 무시됩니다.
@slidestart blood
ncloud
provider 입니다.@slideend
우리는 이 실습에서 사용할 Terraform 코드를 다운로드 했습니다. 나머지 실습에서 이 소스코드를 사용할 것입니다.
Terraform으로 무엇이든 하기 전에 Workspace를 초기화 해야 합니다.
init
명령을 수행합니다.terraform init
+
+...
+Terraform has been successfully initialized!
+
\`terraform init 명령은 Terraform 코드를 스캔하고 필요한 Provider를 식별하고 다운로드합니다.
.terraform
디렉토리에 설치되었는지 확인합니다.이 숨겨진 디렉토리는 모든 모듈과 플러그인이 저장되는 곳입니다.
Q. Terraform은 모듈과 공급자를 어디에 저장합니까?
@slidestart blood
terraform validate
명령으로 실행할 수 있습니다.@slideend
Terraform에는 validate
라는 하위 명령이 내장되어 있습니다. 이것은 코드가 올바르게 구문 분석되는지 확인하기 위해 코드의 빠른 구문 검사를 수행하려는 경우에 유용합니다.
terraform validate
명령을 터미널에서 실행합니다.
terraform validate
+
다시 따옴표 표기를 넣고 저장한 다음 terraform validate
명령을 실행합니다. 이번에는 검증을 통과해야 합니다.
terraform validate
명령은 자동화된 CI/CD 테스트 파이프라인에서 가장 자주 사용됩니다. 다른 단계를 수행하기 전에 코드에서 오류를 빠르게 포착할 수 있습니다.
@slidestart blood
terraform plan
을 통해 환경에 대한 변경 사항을 안전한 방법으로 미리 볼 수 있습니다.@slideend
terraform plan
명령을 실행합니다.이 명령을 실행하면 Terraform에서 prefix
변수 를 입력하라는 메시지를 표시 합니다.
소문자 또는 숫자의 짧은 문자열을 입력합니다. 영문 이니셜 소문자를 사용하는 것이 좋습니다.
prefix
는 현재 Terraform 코드 구성에서 VPC, 서브넷, 서버 등의 리소스 이름의 일부가 됩니다.
@slidestart blood
terraform.tfvars
파일은 사용자가 변수를 구성할 수 있는 편리한 위치입니다.@slideend
Terraform에서 모든 변수는 사용하기 전에 선언되어야 합니다. 변수는 다른 *.tf
파일에서도 선언될 수 있지만 일반적으로 variables.tf
파일에서 선언됩니다. (default)
해당 값은 terraform.tfvars
파일 및 나중에 다른 방법으로 설정할 수 있습니다.
terraform.tfvars
파일을 수정합니다.terraform.tfvars
파일을 열고 prefix
의 줄 시작 부분에 주석 기호 #
를 제거합니다.
yourname
을 원하는 소문자 또는 숫자의 짧은 문자열을 입력합니다.
# terraform.tfvars
+prefix = "yourname"
+
이제 terraform plan
을 다시 실행 합니다. 이번에는 prefix
를 수동으로 입력할 필요가 없습니다.
@slidestart blood
terraform.tfvars
파일에 설정하여 variables.tf
파일에 정의된 모든 변수를 재정의할 수 있습니다.@slideend
이전 실습에서 prefix
를 terraform.tfvars
파일에서 변수를 설정했습니다. ncloud 인프라가 배포될 vpc의 cidr를 결정할 또 다른 변수를 설정해 보겠습니다.
먼저 다른 계획을 실행하여 위치를 변경한 후 어떻게 되는지 비교할 수 있습니다.
terraform plan
+
address_space
정보를 수정합니다.default로 선언되어있는 값 외에 사용자 지정 변수로 변경해봅니다. terraform.tfvars
파일을 열어 address_space
을 추가하고 다시 terraform plan
을 실행해 봅니다. 이번엔 무엇이 다른가요?
# terraform.tfvars
+prefix = "yourname"
+address_space = "10.0.0.0/16"
+
terraform.tfvars
파일은 variables.tf
파일에 선언된 모든 변수에 대한 값을 설정할 수 있음을 기억하십시오.
Q. Terraform 변수는 일반적으로 어디에 선언 됩니까?
`,22),xe={class:"task-list-container"},we=l('',2),Xe={class:"task-list-item"},Te=e("input",{type:"checkbox",class:"task-list-item-checkbox",id:"task-item-7",disabled:"disabled"},null,-1),ye={class:"task-list-item-label",for:"task-item-7"},Ce={href:"http://variable.tf",target:"_blank",rel:"noopener noreferrer"},Pe=e("li",{class:"task-list-item"},[e("input",{type:"checkbox",class:"task-list-item-checkbox",id:"task-item-8",disabled:"disabled"}),e("label",{class:"task-list-item-label",for:"task-item-8"}," terraform.tfvars 파일에서")],-1),Ae={class:"hint-container details"},Se=e("summary",null,"답",-1),Ee={class:"task-list-container"},ze={class:"task-list-item"},Ne=e("input",{type:"checkbox",class:"task-list-item-checkbox",id:"task-item-9",checked:"checked",disabled:"disabled"},null,-1),Le={class:"task-list-item-label",for:"task-item-9"},De={href:"http://variable.tf",target:"_blank",rel:"noopener noreferrer"},We=e("hr",null,null,-1),Ie=e("p",null,"이 장에서 우리는 :",-1),Oe=e("ul",null,[e("li",null,"terraform init 명령을 확인했습니다."),e("li",null,"terraform plan 명령을 확인했습니다."),e("li",null,"변수에 대해 배웠습니다."),e("li",null,"prefix(접두사) 설정을 했습니다.")],-1);function qe(Be,Ke){const o=c("ExternalLinkIcon"),i=c("CodeTabs"),d=c("Tabs");return m(),u("div",null,[T,e("ol",null,[e("li",null,[e("p",null,[a("테라폼 다운로드 사이트 "),e("a",y,[a("https://www.terraform.io/downloads.html"),n(o)]),a(" 로 접속하여, 자신의 환경에 맞는 Terraform을 다운로드 받습니다.")])]),C]),P,A,n(i,{id:"58",data:[{id:"bash"},{id:"zsh"},{id:"windows"}],"tab-id":"shell"},{title0:r(({value:t,isActive:s})=>[a("bash")]),title1:r(({value:t,isActive:s})=>[a("zsh")]),title2:r(({value:t,isActive:s})=>[a("windows")]),tab0:r(({value:t,isActive:s})=>[S]),tab1:r(({value:t,isActive:s})=>[E]),tab2:r(({value:t,isActive:s})=>[]),_:1}),z,N,n(d,{id:"101",data:[{id:"1. Extention 설치"},{id:"2. 적용된 Extention 확인"}]},{title0:r(({value:t,isActive:s})=>[a("1. Extention 설치")]),title1:r(({value:t,isActive:s})=>[a("2. 적용된 Extention 확인")]),tab0:r(({value:t,isActive:s})=>[L]),tab1:r(({value:t,isActive:s})=>[D]),_:1},8,["data"]),W,I,O,e("p",null,[a("링크 : "),e("a",q,[a("https://github.com/ncp-hc/workshop-oss"),n(o)])]),B,e("h3",K,[e("a",M,[e("span",null,[e("a",U,[a("https://www.terraform.io/downloads.html"),n(o)]),a(" 에서 항상 최신 버전의 Terraform을 다운로드할 수 있습니다.")])])]),V,Y,H,e("h3",R,[e("a",$,[e("span",null,[a("단계별 지침은 이 튜토리얼을 확인하세요. "),e("a",G,[a("https://learn.hashicorp.com/terraform/getting-started/install.html"),n(o)])])])]),Q,n(d,{id:"256",data:[{id:"1. 인증키 관리"},{id:"2. 신규 API 인증키 생성"},{id:"3. API 인증키 쌍 확인"}]},{title0:r(({value:t,isActive:s})=>[a("1. 인증키 관리")]),title1:r(({value:t,isActive:s})=>[a("2. 신규 API 인증키 생성")]),title2:r(({value:t,isActive:s})=>[a("3. API 인증키 쌍 확인")]),tab0:r(({value:t,isActive:s})=>[e("ul",null,[e("li",null,[e("a",F,[a("https://www.ncloud.com/"),n(o)]),a("에 로그인하여 마이페이지 메뉴에서 "),Z,a("를 선택합니다.")]),j])]),tab1:r(({value:t,isActive:s})=>[J]),tab2:r(({value:t,isActive:s})=>[ee]),_:1},8,["data"]),ae,n(i,{id:"316",data:[{id:"bash"},{id:"CMD(Win)"},{id:"Powershell(Win)"}],"tab-id":"shell"},{title0:r(({value:t,isActive:s})=>[a("bash")]),title1:r(({value:t,isActive:s})=>[a("CMD(Win)")]),title2:r(({value:t,isActive:s})=>[a("Powershell(Win)")]),tab0:r(({value:t,isActive:s})=>[re]),tab1:r(({value:t,isActive:s})=>[te]),tab2:r(({value:t,isActive:s})=>[se]),_:1},8,["data"]),ne,e("p",null,[e("a",le,[a("https://registry.terraform.io/browse/providers"),n(o)])]),oe,n(i,{id:"426",data:[{id:"Linux/Mac"},{id:"Windows"},{id:"Code Editor"}],"tab-id":"shell"},{title0:r(({value:t,isActive:s})=>[a("Linux/Mac")]),title1:r(({value:t,isActive:s})=>[a("Windows")]),title2:r(({value:t,isActive:s})=>[a("Code Editor")]),tab0:r(({value:t,isActive:s})=>[ie]),tab1:r(({value:t,isActive:s})=>[ce]),tab2:r(({value:t,isActive:s})=>[]),_:1}),de,e("h4",pe,[e("a",he,[e("span",null,[a("💻 "),e("a",me,[a("main.tf"),n(o)]),a(" 파일을 편집합니다.")])])]),ue,fe,be,ve,ge,ke,_e,e("ul",xe,[we,e("li",Xe,[Te,e("label",ye,[e("a",Ce,[a("variable.tf"),n(o)]),a(" 파일에서")])]),Pe]),e("details",Ae,[Se,e("ul",Ee,[e("li",ze,[Ne,e("label",Le,[e("a",De,[a("variable.tf"),n(o)]),a(" 파일에서")])])])]),We,Ie,Oe])}const Ye=h(X,[["render",qe],["__file","02-z-lab_terraform_basic.html.vue"]]),He=JSON.parse('{"path":"/03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/02-z-lab_terraform_basic.html","title":"💻 Lab - Setup and Basic Usage","lang":"ko-KR","frontmatter":{"description":"Naver Cloud Platform에서의 Terraform 실습","tag":["ncloud","ncp","terraform","workshop"],"head":[["meta",{"property":"og:url","content":"https://docmoa.github.io/03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/02-z-lab_terraform_basic.html"}],["meta",{"property":"og:site_name","content":"docmoa"}],["meta",{"property":"og:title","content":"💻 Lab - Setup and Basic Usage"}],["meta",{"property":"og:description","content":"Naver Cloud Platform에서의 Terraform 실습"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://icons.iconarchive.com/icons/bogo-d/project/16/OS-Windows-7-icon.png"}],["meta",{"property":"og:locale","content":"ko-KR"}],["meta",{"property":"og:updated_time","content":"2023-10-25T07:28:39.000Z"}],["meta",{"name":"twitter:card","content":"summary_large_image"}],["meta",{"name":"twitter:image:alt","content":"💻 Lab - Setup and Basic Usage"}],["meta",{"property":"article:tag","content":"ncloud"}],["meta",{"property":"article:tag","content":"ncp"}],["meta",{"property":"article:tag","content":"terraform"}],["meta",{"property":"article:tag","content":"workshop"}],["meta",{"property":"article:modified_time","content":"2023-10-25T07:28:39.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"💻 Lab - Setup and Basic Usage\\",\\"image\\":[\\"https://icons.iconarchive.com/icons/bogo-d/project/16/OS-Windows-7-icon.png\\"],\\"dateModified\\":\\"2023-10-25T07:28:39.000Z\\",\\"author\\":[]}"]]},"headers":[{"level":2,"title":"🏡 Moving in - Explore Your Workspace","slug":"🏡-moving-in-explore-your-workspace","link":"#🏡-moving-in-explore-your-workspace","children":[{"level":3,"title":"Terraform 명령줄 도구는 MacOS, FreeBSD, OpenBSD, Windows, Solaris 및 Linux에서 사용할 수 있습니다.","slug":"terraform-명령줄-도구는-macos-freebsd-openbsd-windows-solaris-및-linux에서-사용할-수-있습니다","link":"#terraform-명령줄-도구는-macos-freebsd-openbsd-windows-solaris-및-linux에서-사용할-수-있습니다","children":[]},{"level":3,"title":"Terraform 언어는 사람과 기계가 읽을 수 있도록 설계되었습니다.","slug":"terraform-언어는-사람과-기계가-읽을-수-있도록-설계되었습니다","link":"#terraform-언어는-사람과-기계가-읽을-수-있도록-설계되었습니다","children":[]},{"level":3,"title":"대부분의 최신 코드 편집기는 Terraform 구문 강조 표시를 지원합니다.","slug":"대부분의-최신-코드-편집기는-terraform-구문-강조-표시를-지원합니다","link":"#대부분의-최신-코드-편집기는-terraform-구문-강조-표시를-지원합니다","children":[]},{"level":3,"title":"테라폼 설치 및 구성","slug":"테라폼-설치-및-구성","link":"#테라폼-설치-및-구성","children":[]},{"level":3,"title":"편집기 구성","slug":"편집기-구성","link":"#편집기-구성","children":[]},{"level":3,"title":"실습을 위한 코드 받기","slug":"실습을-위한-코드-받기","link":"#실습을-위한-코드-받기","children":[]},{"level":3,"title":"편집기에서 열기","slug":"편집기에서-열기","link":"#편집기에서-열기","children":[]}]},{"level":2,"title":"👋 Getting to Know Terraform","slug":"👋-getting-to-know-terraform","link":"#👋-getting-to-know-terraform","children":[{"level":3,"title":"Terraform 오픈 소스는 랩톱 또는 가상 워크스테이션에서 다운로드하여 실행할 수 있는 명령줄 응용 프로그램입니다.","slug":"terraform-오픈-소스는-랩톱-또는-가상-워크스테이션에서-다운로드하여-실행할-수-있는-명령줄-응용-프로그램입니다","link":"#terraform-오픈-소스는-랩톱-또는-가상-워크스테이션에서-다운로드하여-실행할-수-있는-명령줄-응용-프로그램입니다","children":[]},{"level":3,"title":"Go로 작성되었으며 macOS, Linux 또는 Windows에서 실행됩니다.","slug":"go로-작성되었으며-macos-linux-또는-windows에서-실행됩니다","link":"#go로-작성되었으며-macos-linux-또는-windows에서-실행됩니다","children":[]},{"level":3,"title":"https://www.terraform.io/downloads.html 에서 항상 최신 버전의 Terraform을 다운로드할 수 있습니다.","slug":"https-www-terraform-io-downloads-html-에서-항상-최신-버전의-terraform을-다운로드할-수-있습니다","link":"#https-www-terraform-io-downloads-html-에서-항상-최신-버전의-terraform을-다운로드할-수-있습니다","children":[]},{"level":3,"title":"노트북이나 워크스테이션에 Terraform을 설치하는 것은 쉽습니다. zip 파일을 다운로드하고 압축을 풀고 PATH의 어딘가에 배치하기만 하면 됩니다.","slug":"노트북이나-워크스테이션에-terraform을-설치하는-것은-쉽습니다-zip-파일을-다운로드하고-압축을-풀고-path의-어딘가에-배치하기만-하면-됩니다","link":"#노트북이나-워크스테이션에-terraform을-설치하는-것은-쉽습니다-zip-파일을-다운로드하고-압축을-풀고-path의-어딘가에-배치하기만-하면-됩니다","children":[]},{"level":3,"title":"단계별 지침은 이 튜토리얼을 확인하세요. https://learn.hashicorp.com/terraform/getting-started/install.html","slug":"단계별-지침은-이-튜토리얼을-확인하세요-https-learn-hashicorp-com-terraform-getting-started-install-html","link":"#단계별-지침은-이-튜토리얼을-확인하세요-https-learn-hashicorp-com-terraform-getting-started-install-html","children":[]}]},{"level":2,"title":"🔐 Terraform을 NCP에 연결하기","slug":"🔐-terraform을-ncp에-연결하기","link":"#🔐-terraform을-ncp에-연결하기","children":[{"level":3,"title":"HCL이","slug":"hcl이","link":"#hcl이","children":[]},{"level":3,"title":"\\"HashiCorp Configuration Language\\"","slug":"hashicorp-configuration-language","link":"#hashicorp-configuration-language","children":[]},{"level":3,"title":"의 약자라는 것을 알고 계신가요?","slug":"의-약자라는-것을-알고-계신가요","link":"#의-약자라는-것을-알고-계신가요","children":[]},{"level":3,"title":"NCP 자격증명 받기","slug":"ncp-자격증명-받기","link":"#ncp-자격증명-받기","children":[]},{"level":3,"title":"NCP 자격증명 환경변수로 저장하기","slug":"ncp-자격증명-환경변수로-저장하기","link":"#ncp-자격증명-환경변수로-저장하기","children":[]}]},{"level":2,"title":"👨💻 Terraform 코드는 어떻게 생겼나요?","slug":"👨💻-terraform-코드는-어떻게-생겼나요","link":"#👨💻-terraform-코드는-어떻게-생겼나요","children":[{"level":3,"title":"Terraform은 현재 디렉토리에서 *.tf 또는 *.tfvars 로 끝나는 모든 것을 읽습니다.","slug":"terraform은-현재-디렉토리에서-tf-또는-tfvars-로-끝나는-모든-것을-읽습니다","link":"#terraform은-현재-디렉토리에서-tf-또는-tfvars-로-끝나는-모든-것을-읽습니다","children":[]},{"level":3,"title":"일반적으로 Terraform Workspace는 main.tf, variables.tf, outputs.tf 파일로 구성됩니다.","slug":"일반적으로-terraform-workspace는-main-tf-variables-tf-outputs-tf-파일로-구성됩니다","link":"#일반적으로-terraform-workspace는-main-tf-variables-tf-outputs-tf-파일로-구성됩니다","children":[]},{"level":3,"title":"Terraform 코드를 목적에 따라 파일로 그룹화할 수도 있습니다.","slug":"terraform-코드를-목적에-따라-파일로-그룹화할-수도-있습니다","link":"#terraform-코드를-목적에-따라-파일로-그룹화할-수도-있습니다","children":[]}]},{"level":2,"title":"🏡 Terraform Init - Provider 설치","slug":"🏡-terraform-init-provider-설치","link":"#🏡-terraform-init-provider-설치","children":[{"level":3,"title":"Terraform Core 프로그램은 그 자체로는 그다지 유용하지 않습니다.","slug":"terraform-core-프로그램은-그-자체로는-그다지-유용하지-않습니다","link":"#terraform-core-프로그램은-그-자체로는-그다지-유용하지-않습니다","children":[]},{"level":3,"title":"Terraform은 클라우드 API와 통신할 수 있도록 Provider(공급자) 의 도움이 필요합니다.","slug":"terraform은-클라우드-api와-통신할-수-있도록-provider-공급자-의-도움이-필요합니다","link":"#terraform은-클라우드-api와-통신할-수-있도록-provider-공급자-의-도움이-필요합니다","children":[]},{"level":3,"title":"Terraform에는 수백 개의 다양한 Provider가 있습니다. 여기에서 Provider 목록을 찾아볼 수 있습니다.","slug":"terraform에는-수백-개의-다양한-provider가-있습니다-여기에서-provider-목록을-찾아볼-수-있습니다","link":"#terraform에는-수백-개의-다양한-provider가-있습니다-여기에서-provider-목록을-찾아볼-수-있습니다","children":[]},{"level":3,"title":"오늘 우리는 몇 가지 다른 Provider를 사용할 것이지만 주요 Provider는 ncloud provider 입니다.","slug":"오늘-우리는-몇-가지-다른-provider를-사용할-것이지만-주요-provider는-ncloud-provider-입니다","link":"#오늘-우리는-몇-가지-다른-provider를-사용할-것이지만-주요-provider는-ncloud-provider-입니다","children":[]}]},{"level":2,"title":"😱 Quiz Time 1. Provider와 Module","slug":"quiz-time-1-provider와-module","link":"#quiz-time-1-provider와-module","children":[]},{"level":2,"title":"👩⚖️ Terraform Validate - 코드 테스트","slug":"👩⚖️-terraform-validate-코드-테스트","link":"#👩⚖️-terraform-validate-코드-테스트","children":[{"level":3,"title":"Terraform에는 구문 검사기가 내장되어 있습니다.","slug":"terraform에는-구문-검사기가-내장되어-있습니다","link":"#terraform에는-구문-검사기가-내장되어-있습니다","children":[]},{"level":3,"title":"terraform validate 명령으로 실행할 수 있습니다.","slug":"terraform-validate-명령으로-실행할-수-있습니다","link":"#terraform-validate-명령으로-실행할-수-있습니다","children":[]}]},{"level":2,"title":"🤔 Terraform Plan - Dry run mode","slug":"🤔-terraform-plan-dry-run-mode","link":"#🤔-terraform-plan-dry-run-mode","children":[{"level":3,"title":"terraform plan을 통해 환경에 대한 변경 사항을 안전한 방법으로 미리 볼 수 있습니다.","slug":"terraform-plan을-통해-환경에-대한-변경-사항을-안전한-방법으로-미리-볼-수-있습니다","link":"#terraform-plan을-통해-환경에-대한-변경-사항을-안전한-방법으로-미리-볼-수-있습니다","children":[]},{"level":3,"title":"이렇게 하면 이미 빌드된 후가 아니라 배포하기 전에 예기치 않은 변경 사항을 식별하는 데 도움이 될 수 있습니다.","slug":"이렇게-하면-이미-빌드된-후가-아니라-배포하기-전에-예기치-않은-변경-사항을-식별하는-데-도움이-될-수-있습니다","link":"#이렇게-하면-이미-빌드된-후가-아니라-배포하기-전에-예기치-않은-변경-사항을-식별하는-데-도움이-될-수-있습니다","children":[]}]},{"level":2,"title":"🎛️ Terraform 변수로 작업하기","slug":"🎛️-terraform-변수로-작업하기","link":"#🎛️-terraform-변수로-작업하기","children":[{"level":3,"title":"terraform.tfvars 파일은 사용자가 변수를 구성할 수 있는 편리한 위치입니다.","slug":"terraform-tfvars-파일은-사용자가-변수를-구성할-수-있는-편리한-위치입니다","link":"#terraform-tfvars-파일은-사용자가-변수를-구성할-수-있는-편리한-위치입니다","children":[]}]},{"level":2,"title":"🗼 cidr_block 변경","slug":"🗼-cidr-block-변경","link":"#🗼-cidr-block-변경","children":[{"level":3,"title":"개개인은 terraform.tfvars 파일에 설정하여 variables.tf 파일에 정의된 모든 변수를 재정의할 수 있습니다.","slug":"개개인은-terraform-tfvars-파일에-설정하여-variables-tf-파일에-정의된-모든-변수를-재정의할-수-있습니다","link":"#개개인은-terraform-tfvars-파일에-설정하여-variables-tf-파일에-정의된-모든-변수를-재정의할-수-있습니다","children":[]},{"level":3,"title":"이번 실습에서는 ncloud 리소스를 배포하는 위치를 지정합니다.","slug":"이번-실습에서는-ncloud-리소스를-배포하는-위치를-지정합니다","link":"#이번-실습에서는-ncloud-리소스를-배포하는-위치를-지정합니다","children":[]}]},{"level":2,"title":"😱 Quiz Time 2. Variables","slug":"quiz-time-2-variables","link":"#quiz-time-2-variables","children":[]}],"git":{"createdTime":1695042774000,"updatedTime":1698218919000,"contributors":[{"name":"Great-Stone","email":"hahohh@gmail.com","commits":2}]},"readingTime":{"minutes":2.29,"words":687},"filePathRelative":"03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/02-z-lab_terraform_basic.md","localizedDate":"2023년 9월 18일","excerpt":"\\n@slidestart blood
\\n@slideend
\\nPipeline이 수행되는 동작을 추적하는 과정을 확인합니다. 이를 이를 위한 Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 03-01.TrackingBuildState)
Pipeline에 다음과 같이 스크립트를 추가합니다.
pipeline {
+ agent any
+ stages {
+ stage('Deploy') {
+ steps {
+ timeout(time: 1, unit: 'MINUTES') {
+ sh 'for n in \`seq 1 10\`; do echo $n; sleep 1; done'
+ }
+ timeout(time: 1, unit: 'MINUTES') {
+ sh 'for n in \`seq 1 50\`; do echo $n; sleep 1; done'
+ }
+ }
+ }
+ }
+}
+
Build Now
를 클릭하여 빌드를 수행합니다. 그러면, 좌측의 Build History
에 새로운 기록이 생성되면서 동작 중인것을 확인 할 수 있습니다.
첫번째 방법은 앞서 확인한 Pipeline Steps
를 확인하는 것입니다. 다시한번 확인하는 방법을 설명합니다.
Build History
에서 최신 빌드를 클릭합니다.Pipeline Steps
를 클릭하면 Pipeline 수행 스텝을 확인할 수 있습니다.현재 수행중인 Pipeline이 어떤 단계가 수행중인지 각 스탭별로 확인할 수 있고 상태를 확인할 수 있습니다.
두번째 방법은 출력되는 콘솔 로그를 확인하는 것입니다. Jenkins에서 빌드를 수행하면 빌드 수행 스크립트가 내부에 임시적으로 생성되어 작업을 실행합니다. 이때 발생되는 로그는 Console Output
을 통해 거의 실시간으로 동작을 확인 할 수 있습니다.
Build History
에서 최신 빌드에 마우스 포인터를 가져가면 우측에 드롭박스가 생깁니다. 또는 해당 히스토리를 클릭합니다.Console Output
나 클릭된 빌드 히스토리 상태에서 Console Output
를 클릭하면 수행중인 콘솔상의 출력을 확인합니다.마지막으로는 Pipeline을 위한 UI인 BlueOcean
플러그인을 활용하는 방법입니다. Blue Ocean은 Pipeline에 알맞은 UI를 제공하며 수행 단계와 각 단게별 결과를 쉽게 확인할 수 있습니다.
Jenkins 관리
에서 플러그인 관리
를 선택합니다.설치 가능
탭에서 Blue Ocean
을 선택하여 재시작 없이 설치
를 클릭 합니다.Blue Ocean
플러그인만 선택하여 설치하더라도 관련 플러그인들이 함께 설치 진행됩니다.Blue Ocean
항목을 확인 할 수 있습니다.Git SCM을 기반으로 Pipeline을 설정하는 과정을 설명합니다. 이를 이를 위한 Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 03-02.PollingSCMforBuildTriggering)
해당 과정을 수행하기 위해서는 다음의 구성이 필요합니다.
Jenkins가 구성된 호스트에 git 설치
$ yum -y install git
+
Jenkins 설정
Jenkins 관리
의 Global Tool Configuration
클릭Path to Git executable
칸에 Path 입력 (e.g. /usr/bin/git)Pipeline을 다음과 같이 설정합니다.
`,21),f=n("li",null,"Definition : Pipeline script from SCM",-1),v=n("li",null,"SCM : Git",-1),y={href:"https://github.com/Great-Stone/jenkins-git",target:"_blank",rel:"noopener noreferrer"},S=i(`추가로 빌드 트리거를 위한 설정을 합니다.
Build Triggers
의 Poll SCM
활성화
Schedule 등록
# min hour day month day_of_week
+* * * * *
+# will run every minute on the minute
+
Polling으로 인한 빌드 트리거가 동작하면 좌측 메뉴의 Polling Log
에서 상태 확인이 가능합니다.
1분마다 확인 하도록 되어있기 때문에 다시 Polling을 시도하지만 변경사항이 없는 경우에는 Polling Log에 No changes
메시지가 나타나고 빌드는 수행되지 않습니다.
GitHub를 통한 CI 과정을 설명합니다. WebHook의 설정과 Jenkins에 관련 설정은 어떻게 하는지 알아봅니다.
Jenkins에서 접속가능하도록 GitHub에서 Token을 생성합니다.
',9),P={href:"http://github.com",target:"_blank",rel:"noopener noreferrer"},C=i('우측 상단의 드롭박스에서 Settings
선택 후 좌측 메뉴 맨 아래의 Developer settings
를 선택합니다.
Developer settings
화면에서 좌측 메뉴 하단 Personal access tockes
를 클릭하고, 화면이 해당 페이지로 변경되면 Generate new token
버튼을 클릭합니다.
Token description에 Token설명을 입력하고 입니다. (e.g. jenkins-integration) 생성합니다. 생성시 repo
, admin:repo_hook
, notifications
항목은 활성화 합니다.
Generate token
버튼을 클릭하여 Token 생성이 완료되면 발급된 Token을 확인 할 수 있습니다. 해당 값은 Jenkins에서 Git연동설정 시 필요합니다.
ADD
트롭박스를 선택합니다. Secret text
로 선택합니다. ADD
버튼 클릭하여 새로운 Credendial을 추가합니다.시스템 설정
화면으로 나오면 Credentials의 -none-
드롭박스에 추가한 Credential을 선택합니다.TEST CONNECTION
버튼을 클릭하여 정상적으로 연결이 되는지 확인합니다. Credentials verified for user Great-Stone, rate limit: 4998
와같은 메시지가 출력됩니다.우측 상단의 드롭박스에서 Settings
선택 후 좌측 메뉴 맨 아래의 Developer settings
를 선택합니다.
Developer settings
화면에서 좌측 메뉴 하단 Personal access tockes
를 클릭하고, 화면이 해당 페이지로 변경되면 Generate new token
버튼을 클릭합니다.
Token description에 Token설명을 입력하고 입니다. (e.g. jenkins-webhook) 생성합니다. 생성시 repo
, admin:repo_hook
, notifications
항목은 활성화 합니다.
Generate token
버튼을 클릭하여 Token 생성이 완료되면 발급된 Token을 확인 할 수 있습니다. 해당 값은 Jenkins에서 Git연동설정 시 필요합니다.
Webhook을 위한 Pipeline
타입의 Item을 추가로 생성합니다. (e.g. 03-04.WebhookBuild Triggering)
설정은 다음과 같이 수행합니다.
Pipeline
설정의 Definition
의 드롭다운을 선택하여 Pipeline script from SCM
을 선택합니다.
SCM
항목은 Git
을 선택하고 하위 필드를 다음과 같이 정의합니다.
Repositories :
Repository URL
을 입력하는데, GitHub에서 git url을 얻기위해서는 웹브라우저에서 해당 repository로 이동하여 Clone or download
버튼을 클릭하여 Url을 복사하여 붙여넣습니다.
Credentials : ADD
트롭박스를 선택합니다.
Username with password
로 선택합니다. ADD
버튼 클릭하여 새로운 Credendial을 추가합니다.시스템 설정
화면으로 나오면 Credentials의 -none-
드롭박스에 추가한 Credential을 선택합니다.Script Path : Pipeline 스크립트가 작성된 파일 패스를 지정합니다. 예제 소스에서는 root 위치에 Jenkinsfile
로 생성되어있으므로 해당 칸에는 Jenkinsfile
이라고 입력 합니다.
저장 후 좌측 메뉴의 Build Now
를 클릭하면 SCM에서 소스를 받고 Pipeline을 지정한 스크립트로 수행하는 것을 확인 할 수 있습니다.
Pipeline이 수행되는 동작을 추적하는 과정을 확인합니다. 이를 이를 위한 Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 03-01.TrackingBuildState)
\\nPipeline에 다음과 같이 스크립트를 추가합니다.
\\npipeline {\\n agent any\\n stages {\\n stage('Deploy') {\\n steps {\\n timeout(time: 1, unit: 'MINUTES') {\\n sh 'for n in \`seq 1 10\`; do echo $n; sleep 1; done'\\n }\\n timeout(time: 1, unit: 'MINUTES') {\\n sh 'for n in \`seq 1 50\`; do echo $n; sleep 1; done'\\n }\\n }\\n }\\n }\\n}\\n
zip과 tar.gz는 Unix/Linux환경에서 사용할 압축된 형태의 파일이고 나머지 네가지는 CPU 아키텍쳐에 맞게 컴파일된 zip 형태의 압축 파일과 서비스에 등록할수 있는 형태의 인스톨러로 구성되어 있습니다. 설치하고자 하는 OS와 CPU 아키텍처에 맞는 설치파일을 받아 준비합니다.
Windows에는 압축파일을 풀어 설치하는 방법과 서비스 등록을 위한 인스톨러 두가지 방식이 있었습니다. 압축파일 형태의 경우 압축을 풀기만하면 바로 실행이 가능합니다. 서비스 인스톨러의 경우 서비스에 등록하기 위한 설치 경로와 같은 정보를 입력하여 진행합니다.
압축파일로 설치한 경우 %CATALINA_HOME%\\bin
에 위치한 startup.bat
으로 시작하고 shutdown.bat
으로 종료합니다.
서비스로 설치한 경우 윈도우 서비스 관리 유틸리티에서 서비스의 '시작/종료'를 사용하거나 net start (서비스이름)
또는 net stop (서비스이름)
을 사용하여 서비스의 시작과 종료가 가능합니다.
Unix/Linux의 binary 설치파일은 압축을 풀어 설치합니다.
$CATALINA_HOME/bin
에 위치한 startup.sh
으로 시작하고 shutdown.sh
으로 종료합니다.
설치 방법은 매우 간단하나 설치 후 꼭 해야할 작업이 있습니다. Java Home을 설정하고 성능을 위한 Native library를 설치하는 작업 입니다.
Java Home의 경우 앞서 JDK를 OS에 설치하였다면 톰캣에서 이를 사용할 수 있도록 경로를 잡아주는 과정입니다. OS자체의 PATH나 환경변수로 지정하는 방법도 있고 톰캣의 스크립트에 변수로 넣어주는 방법이 있습니다. OS자체의 PATH로 설정하는 경우 해당 OS에 설치된 모든 Java 실행환경이 영향을 받게 됩니다. 따라서 일관된 서비스, 일관된 톰캣 운영 환경인 경우 같은 Java Home이 설정되는 장점이 있습니다. 이와달리 스크립트에 Java Home을 설정하면 해당 톰캣에서만 관련 설정의 영향을 받습니다. OS내에 서로 다른 JDK로 동작하는 서비스나 어플리케이션이 있다면 스크립트를 이용하는 방법을 사용할 수 있습니다.
OS 환경에 Java Home을 설정하는 방법은 다음과 같습니다. (Java Home은 JDK의 bin 디렉토리를 포함한 상위 디렉토리 입니다.)
Windows환경에서 Java를 C:\\Program Files
에 설치하는 경우 중간에 공백이 있기 때문에 C:\\Progra~1
로 표현함에 주의합니다.
톰캣의 Native Library를 적용하지 않고도 충분히 톰캣을 실행하고 사용할 수 있습니다. 다만 톰캣의 콘솔 로그에 다음의 메시지가 걸리적(?)거리게 발생합니다.
The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path
+
굳이 필요하지 않다면 적용하지 않아도 되지만 Native Library가 주는 이점은 다음과 같습니다.
결국 NIO(Non-Blocking I/O)를 지원하는 것과 SSL관련 설정을 지원합니다. OpenSSL을 사용하지 않아도 NIO를 사용하는 성능상의 이점을 제공합니다.
Native Library를 사용하기 위해서는 APR과 Tomcat Native 소스 파일이 필요합니다. APR은 Apache 2.0 이상 버전이 설치되어있다면 해당 설치 경로의 $APACHE_HOME/bin/apr-1-config
과 같이 존재하나 없는 경우 별도의 APR을 설치합니다.
APR 홈페이지에서 받은 apr-x.x.x.tar.gz 파일은 다음의 순서를 따라 설치를 진행하며, 예제와 같이 /usr/local
에 설치하는 경우 root 계정이 필요합니다.
$ gzip -d apr-1.5.1.tar.gz
+$ tar -xvf apr-1.5.1.tar
+$ configure --prefix=/usr/local/apr-1.5.1
+$ make
+$ make install
+
Native Library는 다음과 같이 설치를 진행합니다.
$ gzip -d tomcat-native.tar.gz
+$ tar -xvf tomcat-native.tar
+$ configure --prefix=$CATALINA_HOME --with-apr=/usr/local/apr-1.5.1/bin/apr-1-config --with-java-home=$JAVA_HOME
+$ make
+$ install
+
소스의 컴파일과 설치가 완료되면 "$CATALINA_HOME/lib"에서 libtcnative 관련 파일의 확인을 할 수 있습니다.
`,3),q={href:"http://setenv.sh",target:"_blank",rel:"noopener noreferrer"},U=i(`# setenv.sh(bat)
+LD_LIBRARY_PATH "$CATALINA_HOME/lib"
+
OS | Shared Library Path |
---|---|
Windows | PATH |
Solaris | LD_LIBRARY_PATH |
HP-UX | SHLIB_PATH |
AIX | LIBPATH,LD_LIBRARY_PATH |
Linux | LD_LIBRARY_PATH |
OS/2 | LIBPATH |
OSX | LD_LIBRARY_PATH |
적용된 Native Library는 톰캣의 콘솔 로그 "$CATALINA_HOME/logs/catalina.out(log)"에서 다음의 메시지를 확인 할 수 있습니다.
Info: Loaded APR based Apache Tomcat Native library 1.1.31.
+
모든 Terraform으로 구성되는 리소스는 정확히 동일한 방식으로 구성됩니다.
resource type "name" {
+ parameter = "foo"
+ parameter2 = "bar"
+ list = ["one", "two", "three"]
+}
+
ncloud_vpc
Terraform Core는 무엇이든 빌드하려면 하나 이상의 Provider가 필요합니다.
사용하려는 Provider의 버전을 쑤동으로 구성 할 수 있습니다. 이 옵션을 비워두면 Terraform은 기본적으로 사용 가능한 최신 버전의 Provider를 사용합니다.
`,8),m=n("div",{class:"language-hcl line-numbers-mode","data-ext":"hcl","data-title":"hcl"},[n("pre",{hcl:"",class:"language-hcl"},[n("code",null,[n("span",{class:"token keyword"},"terraform"),a(),n("span",{class:"token punctuation"},"{"),a(` + `),n("span",{class:"token keyword"},"required_providers"),a(),n("span",{class:"token punctuation"},"{"),a(` + `),n("span",{class:"token property"},"ncloud"),a(),n("span",{class:"token punctuation"},"="),a(),n("span",{class:"token punctuation"},"{"),a(` + `),n("span",{class:"token property"},"source"),a(" "),n("span",{class:"token punctuation"},"="),a(),n("span",{class:"token string"},'"NaverCloudPlatform/ncloud"'),a(` + `),n("span",{class:"token property"},"version"),a(),n("span",{class:"token punctuation"},"="),a(),n("span",{class:"token string"},'">= 2.1.2"'),a(` + `),n("span",{class:"token punctuation"},"}"),a(` + `),n("span",{class:"token punctuation"},"}"),a(` +`),n("span",{class:"token punctuation"},"}"),a(` + +`),n("span",{class:"token keyword"},[a("provider"),n("span",{class:"token type variable"},' "ncloud" ')]),n("span",{class:"token punctuation"},"{"),a(),n("span",{class:"token punctuation"},"}"),a(` +`)])]),n("div",{class:"highlight-lines"},[n("br"),n("br"),n("br"),n("br"),n("div",{class:"highlight-line"}," "),n("br"),n("br"),n("br"),n("br"),n("br")]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),k=s('terraform destroy
는 action
과 반대 입니다. 승인하면 인프라가 제거됩니다.
Terraform은 내장 된 코드 포맷터/클리너와 함께 제공됩니다. 모든 여백과 목록 들여 쓰기를 깔끔하고 깔끔하게 만들 수 있습니다. 아름다운 코드가 더 잘 동작하는 것(?) 같습니다.
terraform fmt
+
*.tf
파일이 포함 된 디렉토리에서 실행하기 만하면 코드가 정리됩니다.
data "ncloud_member_server_images" "prod" {
+ filter {
+ name = "name"
+ values = [data.terraform_remote_state.image_name.outputs.image_name]
+ }
+}
+
+resource "ncloud_server" "server" {
+ name = "${var.server_name}${random_id.id.hex}"
+ member_server_image_no = data.ncloud_member_server_images.prod.member_server_images.0
+ server_product_code = "SPSVRGPUSSD00001"
+ login_key_name = ncloud_login_key.key.key_name
+ zone = var.zone
+}
+
Data Source(data)는 Provider가 기존 리소스를 반환하도록 쿼리하는 방법입니다.
생성되어있는 리소스나 Provider로 조회할 수 있는 리소스 정보를 다른 리소스 구성에서 접근할 수 있습니다.
Terraform은 자동으로 종속성을 추적 할 수 있습니다. 앞서 설명된 리소스를 살펴보십시오. ncloud_server 리소스에서 강조 표시된 줄을 확인합니다. 이것이 테라 폼에서 한 리소스가 다른 리소스를 참조하도록하는 방법입니다.
Terraform은 Workspace에서 .tf
확장자로 끝나는 모든 파일을 읽지만 대표적으로는 main.tf
, variables.tf
, outputs.tf
를 갖는 것입니다. 원하는 경우 더 많은 tf 파일을 추가 할 수 있습니다.
파일 구조
Workspace
+├── \`main.tf\`
+├── \`outputs.tf\`
+├── terraform.tfvars
+└── \`variables.tf\`
+
이러한 각 파일을 자세히 살펴 보겠습니다.
main.tf
파일첫 번째 파일은 main.tf
입니다. 일반적으로 테라 폼 코드를 저장하는 곳입니다. 더 크고 복잡한 인프라를 사용하면이를 여러 파일로 나눌 수 있습니다.
resource "ncloud_vpc" "main" {
+ ipv4_cidr_block = var.address_space
+ name = lower("${var.prefix}-vpc-${var.region}")
+}
+
+resource "ncloud_subnet" "main" {
+ name = "${var.name_scn02}-public"
+ vpc_no = ncloud_vpc.vpc_scn_02.id
+ subnet = cidrsubnet(ncloud_vpc.main.ipv4_cidr_block, 8, 0)
+ zone = "KR-2"
+ network_acl_no = ncloud_network_acl.network_acl_02_public.id
+ subnet_type = "PUBLIC"
+}
+
+...생략...
+
variable.tf
파일두 번째 파일은 variables.tf
입니다. 여기에서 변수를 정의하고 선택적으로 일부 기본값을 설정합니다.
variable "prefix" {
+ description = "This prefix will be included in the name of most resources."
+}
+
+variable "region" {
+ description = "The region where the resources are created."
+ default = "KR"
+}
+
output.tf
파일output.tf
파일은 테라 폼 적용이 끝날 때 표시 할 메시지 또는 데이터를 구성하는 곳입니다.
output "acl_public_id" {
+ value = ncloud_network_acl.network_acl_public.id
+}
+
+output "public_addr" {
+ value = "http://${ncloud_public_ip.main.public_ip}:8080"
+}
+
terraform 리소스 그래프는 리소스 간의 종속성을 시각적으로 보여줍니다.
Region
및 Prefix
변수는 리소스 그룹을 만드는 데 필요하며 이는 가상 네트워크를 구축하는 데 필요합니다.
실습을 위해 다음장으로 이동하세요.
',20);function q(w,T){const e=o("RouteLink");return r(),p("div",null,[d,m,k,v,b,h,g,f,_,y,n("p",null,[l(e,{to:"/03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/03-z-lab_terraform_action.html"},{default:c(()=>[a("💻 Lab - Terraform in Action")]),_:1})])])}const C=t(u,[["render",q],["__file","03-terraform-in-Action.html.vue"]]),D=JSON.parse('{"path":"/03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/03-terraform-in-Action.html","title":"03. 테라폼 실행","lang":"ko-KR","frontmatter":{"description":"Naver Cloud Platform에서의 Terraform 실습","tag":["ncloud","ncp","terraform","workshop"],"head":[["meta",{"property":"og:url","content":"https://docmoa.github.io/03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/03-terraform-in-Action.html"}],["meta",{"property":"og:site_name","content":"docmoa"}],["meta",{"property":"og:title","content":"03. 테라폼 실행"}],["meta",{"property":"og:description","content":"Naver Cloud Platform에서의 Terraform 실습"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"ko-KR"}],["meta",{"property":"og:updated_time","content":"2023-09-19T11:31:31.000Z"}],["meta",{"property":"article:tag","content":"ncloud"}],["meta",{"property":"article:tag","content":"ncp"}],["meta",{"property":"article:tag","content":"terraform"}],["meta",{"property":"article:tag","content":"workshop"}],["meta",{"property":"article:modified_time","content":"2023-09-19T11:31:31.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"03. 테라폼 실행\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2023-09-19T11:31:31.000Z\\",\\"author\\":[]}"]]},"headers":[{"level":2,"title":"리소스 분석","slug":"리소스-분석","link":"#리소스-분석","children":[]},{"level":2,"title":"Terraform Provider 구성","slug":"terraform-provider-구성","link":"#terraform-provider-구성","children":[]},{"level":2,"title":"버전관리 연산자","slug":"버전관리-연산자","link":"#버전관리-연산자","children":[]},{"level":2,"title":"Terraform Apply","slug":"terraform-apply","link":"#terraform-apply","children":[]},{"level":2,"title":"Terraform Destroy","slug":"terraform-destroy","link":"#terraform-destroy","children":[]},{"level":2,"title":"Terraform Format","slug":"terraform-format","link":"#terraform-format","children":[]},{"level":2,"title":"Terraform Data Sources","slug":"terraform-data-sources","link":"#terraform-data-sources","children":[]},{"level":2,"title":"Terraform Dependency Mapping","slug":"terraform-dependency-mapping","link":"#terraform-dependency-mapping","children":[]},{"level":2,"title":"Terraform 코드 구성","slug":"terraform-코드-구성","link":"#terraform-코드-구성","children":[{"level":3,"title":"main.tf 파일","slug":"main-tf-파일","link":"#main-tf-파일","children":[]},{"level":3,"title":"variable.tf 파일","slug":"variable-tf-파일","link":"#variable-tf-파일","children":[]},{"level":3,"title":"output.tf 파일","slug":"output-tf-파일","link":"#output-tf-파일","children":[]}]},{"level":2,"title":"Terraform Dependency Graph","slug":"terraform-dependency-graph","link":"#terraform-dependency-graph","children":[]}],"git":{"createdTime":1695042774000,"updatedTime":1695123091000,"contributors":[{"name":"Great-Stone","email":"hahohh@gmail.com","commits":2}]},"readingTime":{"minutes":1.35,"words":406},"filePathRelative":"03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/03-terraform-in-Action.md","localizedDate":"2023년 9월 18일","excerpt":"\\n모든 Terraform으로 구성되는 리소스는 정확히 동일한 방식으로 구성됩니다.
\\nresource type \\"name\\" {\\n parameter = \\"foo\\"\\n parameter2 = \\"bar\\"\\n list = [\\"one\\", \\"two\\", \\"three\\"]\\n}\\n
git clone
git clone https://github.com/hashicorp/learn-consul-kubernetes.git
+
Code download
다운로드 후 learn-consul-kubernetes/service-mesh/deploy
경로로 이동하고 샘플 구성을 반영합니다.
cd learn-consul-kubernetes/service-mesh/deploy
+kubectl apply -f hashicups/
+
# 출력
+service/frontend created
+serviceaccount/frontend created
+servicedefaults.consul.hashicorp.com/frontend created
+configmap/nginx-configmap created
+deployment.apps/frontend created
+service/postgres created
+serviceaccount/postgres created
+servicedefaults.consul.hashicorp.com/postgres created
+deployment.apps/postgres created
+service/product-api created
+serviceaccount/product-api created
+servicedefaults.consul.hashicorp.com/product-api created
+configmap/db-configmap created
+deployment.apps/product-api created
+service/public-api created
+serviceaccount/public-api created
+servicedefaults.consul.hashicorp.com/public-api created
+deployment.apps/public-api created
+
서비스는 Consul이 각 서비스에 대한 프록시를 자동으로 삽입할 수 있도록 하는 annotation
을 사용합니다. 프록시 는 Consul의 구성을 기반으로 서비스 간의 요청을 처리하기 위해 데이터 플레인을 생성합니다. Consul이 주입되는 label을 선택하여 프록시가 있는 응용 프로그램을 확인할 수 있습니다.
kubectl get pods --selector consul.hashicorp.com/connect-inject-status=injected
+
# 출력
+NAME READY STATUS RESTARTS AGE
+frontend-98cb6859b-6ndvk 2/2 Running 0 3m10s
+postgres-6ccb6d9968-hkbgz 2/2 Running 0 3m9s
+product-api-6798bc4b4d-9ddv4 2/2 Running 2 3m9s
+public-api-5bdf986897-tlxj2 2/2 Running 0 3m9s
+
배포된 앱에 접근하기 위해 port-forward
를 구성합니다.
kubectl port-forward service/frontend 18080:80 --address 0.0.0.0
+
# 출력
+Forwarding from 0.0.0.0:18080 -> 80
+
현재 Intention
규칙이 모두 deny
로 구성되어있다면 에러 화면을 확인하게 됩니다.
UI에서가 아닌 CRD를 통해 Intention
을 정의하기위해 아래와 같이 구성합니다.
cat > ./service-to-service.yaml <<EOF
+apiVersion: consul.hashicorp.com/v1alpha1
+kind: ServiceIntentions
+metadata:
+ name: frontend-to-public-api
+spec:
+ destination:
+ name: public-api
+ sources:
+ - name: frontend
+ action: allow
+---
+apiVersion: consul.hashicorp.com/v1alpha1
+kind: ServiceIntentions
+metadata:
+ name: public-api-to-product-api
+spec:
+ destination:
+ name: product-api
+ sources:
+ - name: public-api
+ action: allow
+---
+apiVersion: consul.hashicorp.com/v1alpha1
+kind: ServiceIntentions
+metadata:
+ name: product-api-to-postgres
+spec:
+ destination:
+ name: postgres
+ sources:
+ - name: product-api
+ action: allow
+EOF
+
규칙의 내용은 다음과 같습니다.
규칙을 적용합니다.
kubectl apply -f service-to-service.yaml
+
# 출력
+serviceintentions.consul.hashicorp.com/frontend-to-public-api created
+serviceintentions.consul.hashicorp.com/public-api-to-product-api created
+serviceintentions.consul.hashicorp.com/product-api-to-postgres created
+
Consul UI에서 확인해보면 해당 Intention 규칙은 CRD로 적용되었기 때문에 Managed by CRD
표시가 붙는것을 확인할 수 있습니다.
배포된 앱에 접근하기 위해 port-forward
를 구성합니다.
kubectl port-forward service/frontend 18080:80 --address 0.0.0.0
+
# 출력
+Forwarding from 0.0.0.0:18080 -> 80
+
서비스 간 연결이 허용되었으므로 페이지가 잘 표시됩니다.
다음 과정을 위해 배포된 리소스를 정리합니다.
kubectl delete -f service-to-service.yaml
+
# 출력
+serviceintentions.consul.hashicorp.com "frontend-to-public-api" deleted
+serviceintentions.consul.hashicorp.com "public-api-to-product-api" deleted
+serviceintentions.consul.hashicorp.com "product-api-to-postgres" deleted
+
kubectl delete -f hashicups/
+
# 출력
+service "frontend" deleted
+serviceaccount "frontend" deleted
+servicedefaults.consul.hashicorp.com "frontend" deleted
+configmap "nginx-configmap" deleted
+deployment.apps "frontend" deleted
+service "postgres" deleted
+serviceaccount "postgres" deleted
+servicedefaults.consul.hashicorp.com "postgres" deleted
+deployment.apps "postgres" deleted
+service "product-api" deleted
+serviceaccount "product-api" deleted
+servicedefaults.consul.hashicorp.com "product-api" deleted
+configmap "db-configmap" deleted
+deployment.apps "product-api" deleted
+service "public-api" deleted
+serviceaccount "public-api" deleted
+servicedefaults.consul.hashicorp.com "public-api" deleted
+deployment.apps "public-api" deleted
+
\\n\\n\\n"}');export{Z as comp,G as data}; diff --git a/assets/03-z-lab_terraform_action.html-Cew6P_D8.js b/assets/03-z-lab_terraform_action.html-Cew6P_D8.js new file mode 100644 index 0000000000..c96c9f8c09 --- /dev/null +++ b/assets/03-z-lab_terraform_action.html-Cew6P_D8.js @@ -0,0 +1,56 @@ +import{_ as p}from"./lab1-02-By4gwc7V.js";import{_ as d}from"./plugin-vue_export-helper-DlAUqK2U.js";import{r as i,o as h,c as u,b as e,d as a,a as r,w as s,e as n}from"./app-Bzk8Nrll.js";const m="/assets/graphviz-1-CABq7Zn_.svg",f="/assets/lab2-01-CIFl3A_H.png",b="/assets/lab2-02-DGoHp863.png",k="/assets/lab2-03-CSmxmP8v.gif",g="/assets/graphviz-2-DN7qG3J4.svg",_={},v=n('
Open Folder...
를 클릭합니다.lab02
을 열어줍니다.@slidestart blood
@slideend
💻 다음 terraform graph명령을 실행해 보세요.
새로운 Workspace 이므로, terraform init
을 수행합니다.
terraform init
+
terraform graph
를 수행합니다.
terraform graph
+
그러면 digraph
로 시작하는 인프라의 시각적 맵을 만드는 데 사용할 수 있는 코드가 생성됩니다. 그래프 데이터는 DOT 그래프 설명 언어 형식 입니다. 무료 Blast Radius 도구를 포함하여 이 데이터를 시각화하는 데 사용할 수 있는 몇 가지 그래프 도구가 있습니다.
경고
plan 정보에는 인증키, 패스워드같은 노출하고 싶지 않은 정보가 포함될 수 있습니다.
@slidestart blood
terraform apply
명령은 Terraform Plan
을 실행하여 원하는 변경 사항을 보여줍니다.@slideend
어떤 일이 일어날지 보려면 먼저 terraform plan
명령을 실행하십시오 .
terraform plan
+
계획 출력에 적절한 prefix, subnet cidr이 표시되는지 확인합니다. 원한다면 terraform.tfvars
혹은 variables.tf
에 정의된 default
값을 변경해보세요.
그런 다음 terraform apply
를 실행하고 리소스가 구축되는 것을 지켜보십시오.
terraform apply
+
Terraform에서 "Do you want to perform these actions?"라는 메시지가 표시되면 yes
를 입력해야 합니다.
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
메시지를 확인하였습니까? 에러가 발생하였다면 무엇이 문제인지 찾아보세요.
지금 우리 코드는 VPC만 정의합니다. 우리는 진행되는 실습에서 이 코드와 프로비저닝된 상태 기반으로 시작 할 것입니다.
`,17),A={href:"https://console.ncloud.com/vpc-network/vpc",target:"_blank",rel:"noopener noreferrer"},D=e("figure",null,[e("img",{src:f,alt:"",tabindex:"0",loading:"lazy"}),e("figcaption")],-1),G=e("figure",null,[e("img",{src:b,alt:"",tabindex:"0",loading:"lazy"}),e("figcaption")],-1),R=e("hr",null,null,-1),V=e("h2",{id:"👩💻-test-and-repair",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#👩💻-test-and-repair"},[e("span",null,"👩💻 Test and Repair")])],-1),N=e("p",null,"@slidestart blood",-1),q=e("h3",{id:"terraform은-멱등성-idempotent-을-갖습니다",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#terraform은-멱등성-idempotent-을-갖습니다"},[e("span",null,"Terraform은 멱등성(idempotent)을 갖습니다.")])],-1),S=e("hr",null,null,-1),L=e("br",null,null,-1),O={href:"https://en.wikipedia.org/wiki/Idempotence",target:"_blank",rel:"noopener noreferrer"},B=n(`@slideend
어떤 일이 일어날지 보려면 먼저 terraform plan
명령을 실행하십시오.
terraform plan
+
VPC가 이미 구축되었으므로 Terraform은 변경이 필요하지 않다고 보고합니다.
이는 정상적이며 예상된 것입니다. 이제 다른 명령인 terraform apply
를 실행하고 지켜보십시오.
terraform apply
+
이미 올바르게 프로비저닝된 경우 VPC를 다시 생성하지 않습니다.
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
@slidestart blood
Terraform은 항상 현재 인프라를 코드에 정의된 것과 일치시키려고 합니다.
@slideend
terraform.tfvars
를 변경합니다.terraform.tfvars
파일을 편집하여 prefix
를 기존과 다른 값으로 변경합니다.
변경 후 terraform apply
를 실행하고 지켜보십시오.
terraform plan
+
VPC가 이미 구축되었으므로 Terraform은 변경이 필요하지 않다고 보고합니다.
이는 정상적이며 예상된 것입니다. 이제 다른 명령인 terraform apply
를 실행하고 지켜보십시오.
terraform apply
+
Terraform에서 "Do you want to perform these actions?"라는 메시지가 표시되면 yes
를 입력하고 완료되기를 기다립니다. 출력의 결과가 어떤가요?
@slidestart blood
@slideend
ncloud_network_acl
을 추가합니다.main.tf
파일을 열고 리소스 블록의 주석처리를 제거하려고 합니다.
리소스 유형은 ncloud_network_acl
이고 이름은 public
입니다.
각 줄의 시작 부분에서 #
문자를 제거하여 코드의 주석 처리를 제거합니다.
코드편집기에서는 주석처리를 위해 해당 라인을 선택하고 활성/비활성 할 수 있습니다.
Mac : ⌘ + /
Win : Ctrl + /
주석 제거 후 파일을 저장하세요.
변경 후 terraform apply
를 실행하고 yes
를 입력하여 추가된 리소스가 생성되는지 확인하세요.
ncloud_network_acl
리소스 내부의 vpc_no
파라메터를 확인합니다. 어떻게 가르키고 있나요?
해당 리소스는 VPC의 설정을 상속 받습니다.
Terraform은 수백개의 상호 연결되 리소스 간의 복잡한 종송석을 맵핑할 수 있습니다.
ncloud_network_acl
을 설정을 변경합니다.ncloud_network_acl
항목에 대해 description
의 내용을 수정해 보세요.
변경 후 terraform apply
를 실행하고 yes
를 입력하여 변경된 사항에 대해 리소스가 어떻게 되는지 확인하세요.
올바르게 프로비저닝된 경우 ncloud_network_acl
를 삭제 후 다시 생성하지 않습니다.
Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
@slidestart blood
-auto-approve
플래그해당 플래그를 사용하여 "Do you want to perform these actions?" 에 한 질문을 오버라이드(Override) 할 수 있습니다.
검토 단계인 Plan을 건너뛰고 바로 Create/Update/Destroy 합니다.
@slideend
main.tf
의 모든 주석을 제거하세요.outputs.tf
의 모든 주석을 제거하세요.terraform plan
를 실행하여 구성할 리소스 항목을 확인합니다.
terraform plan
+
이제 Apply를 실행하여 HashiCat 애플리케이션을 빌드합니다.
terraform apply -auto-approve
+
애플리케이션이 배포를 완료하는데 5~10분이 소요될 수 있습니다. 실행이 끝날 때 애플리케이션 URL이 포함된 Terraform 출력을 보면 완료되었음을 알 수 있습니다.
catapp_url
출력 에서 URL을 클릭하여 새 브라우저 탭에서 웹 애플리케이션을 엽니다.
경고
응용 프로그램이 로드되지 않으면 terraform apply
다시 실행 하십시오. 이렇게 하면 웹 서버를 다시 설치하고 실행 중이 아닌 경우 응용 프로그램을 시작하려고 합니다.
terraform graph를 수행합니다.
terraform graph
+
인프라에 대한 Terraform 그래프를 살펴보십시오. 종속성이 자동으로 매핑됩니다.
Terraform은 이 그래프를 사용하여 최대 효율성을 위해 병렬로 구축할 수 있는 리소스를 결정합니다.
Q. Plan 파일을 지정하지 않고 terraform apply를 실행하면 어떻게 됩니까?
이 장에서 우리는 :
',10),H=e("li",null,"Terraform 리소스에 대해 배웠습니다.",-1),M=e("li",null,"Terraform Plan, Graph, Apply, Destory",-1),Q=e("li",null,"종속성에 대해 배웠습니다.",-1),J=e("li",null,"실습에서 그래프를 확인해보았습니다.",-1),K={href:"http://main.tf",target:"_blank",rel:"noopener noreferrer"},Y={href:"http://variables.tf",target:"_blank",rel:"noopener noreferrer"},$=e("li",null,"Meow World 애플리케이션을 구축하였습니다.",-1);function j(X,ee){const t=i("ExternalLinkIcon"),c=i("Tabs");return h(),u("div",null,[v,e("ul",null,[e("li",null,[e("a",y,[a("Blast Radius"),r(t)]),a("는 설치형 툴입니다.")]),e("li",null,[a("Graphviz는 DOT 그래프 설명 언어를 표시해주는 툴입니다. "),e("ul",null,[e("li",null,[e("a",x,[a("https://dreampuf.github.io/GraphvizOnline/"),r(t)]),a(" 에 앞서 "),T,a("로 시작하는 내용을 복사하여 붙여넣고 어떤 그림이 나오는지 확인해 봅니다.")]),C])])]),w,e("ul",null,[e("li",null,[e("a",P,[a("https://hieven.github.io/terraform-visual/"),r(t)])])]),z,e("p",null,[e("a",A,[a("NCP Consul"),r(t)]),a(" 화면에 접속해 보세요. 구성한 자원이 생성된 것이 확인되나요?")]),r(c,{id:"134",data:[{id:"1. Products & Services"},{id:"2. VPC"}]},{title0:s(({value:o,isActive:l})=>[a("1. Products & Services")]),title1:s(({value:o,isActive:l})=>[a("2. VPC")]),tab0:s(({value:o,isActive:l})=>[D]),tab1:s(({value:o,isActive:l})=>[G]),_:1},8,["data"]),R,V,N,q,S,e("p",null,[a("멱등은 수학 및 컴퓨터 과학의 특정 연산의 속성으로, 초기 적용을 넘어 동일하다면 결과를 변경하지 않고 여러 번 적용할 수 있습니다."),L,a(" 참고 : "),e("a",O,[a("https://en.wikipedia.org/wiki/Idempotence"),r(t)])]),B,U,W,F,I,e("p",null,[e("a",Z,[a("https://dreampuf.github.io/GraphvizOnline/"),r(t)]),a("에 앞서 digraph로 시작하는 내용을 복사하여 붙여넣고 어떤 그림이 나오는지 확인해 봅니다.")]),E,e("ul",null,[H,M,Q,J,e("li",null,[e("a",K,[a("main.tf"),r(t)]),a(", "),e("a",Y,[a("variables.tf"),r(t)]),a(", outputs.tf를 살펴보았습니다.")]),$])])}const ne=d(_,[["render",j],["__file","03-z-lab_terraform_action.html.vue"]]),se=JSON.parse('{"path":"/03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/03-z-lab_terraform_action.html","title":"💻 Lab - Terraform in Action","lang":"ko-KR","frontmatter":{"description":"Naver Cloud Platform에서의 Terraform 실습","tag":["ncloud","ncp","terraform","workshop"],"head":[["meta",{"property":"og:url","content":"https://docmoa.github.io/03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/03-z-lab_terraform_action.html"}],["meta",{"property":"og:site_name","content":"docmoa"}],["meta",{"property":"og:title","content":"💻 Lab - Terraform in Action"}],["meta",{"property":"og:description","content":"Naver Cloud Platform에서의 Terraform 실습"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"ko-KR"}],["meta",{"property":"og:updated_time","content":"2023-10-25T07:28:39.000Z"}],["meta",{"property":"article:tag","content":"ncloud"}],["meta",{"property":"article:tag","content":"ncp"}],["meta",{"property":"article:tag","content":"terraform"}],["meta",{"property":"article:tag","content":"workshop"}],["meta",{"property":"article:modified_time","content":"2023-10-25T07:28:39.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"💻 Lab - Terraform in Action\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2023-10-25T07:28:39.000Z\\",\\"author\\":[]}"]]},"headers":[{"level":3,"title":"편집기에서 열기","slug":"편집기에서-열기","link":"#편집기에서-열기","children":[]},{"level":2,"title":"📈 Terraform Graph","slug":"📈-terraform-graph","link":"#📈-terraform-graph","children":[{"level":3,"title":"Terraform Graph는 모든 인프라에 대한 시각적 표현을 제공할 수 있습니다.","slug":"terraform-graph는-모든-인프라에-대한-시각적-표현을-제공할-수-있습니다","link":"#terraform-graph는-모든-인프라에-대한-시각적-표현을-제공할-수-있습니다","children":[]},{"level":3,"title":"이는 변경의 영향을 받을 종속성 문제 또는 리소스를 찾는 데 유용합니다.","slug":"이는-변경의-영향을-받을-종속성-문제-또는-리소스를-찾는-데-유용합니다","link":"#이는-변경의-영향을-받을-종속성-문제-또는-리소스를-찾는-데-유용합니다","children":[]}]},{"level":2,"title":"👨💻 Terraform Plan & Terraform Apply","slug":"👨💻-terraform-plan-terraform-apply","link":"#👨💻-terraform-plan-terraform-apply","children":[{"level":3,"title":"기본적으로 terraform apply 명령은 Terraform Plan을 실행하여 원하는 변경 사항을 보여줍니다.","slug":"기본적으로-terraform-apply-명령은-terraform-plan을-실행하여-원하는-변경-사항을-보여줍니다","link":"#기본적으로-terraform-apply-명령은-terraform-plan을-실행하여-원하는-변경-사항을-보여줍니다","children":[]},{"level":3,"title":"이는 변경의 영향을 받을 종속성 문제 또는 리소스를 찾는 데 유용합니다.","slug":"이는-변경의-영향을-받을-종속성-문제-또는-리소스를-찾는-데-유용합니다-1","link":"#이는-변경의-영향을-받을-종속성-문제-또는-리소스를-찾는-데-유용합니다-1","children":[]}]},{"level":2,"title":"👩💻 Test and Repair","slug":"👩💻-test-and-repair","link":"#👩💻-test-and-repair","children":[{"level":3,"title":"Terraform은 멱등성(idempotent)을 갖습니다.","slug":"terraform은-멱등성-idempotent-을-갖습니다","link":"#terraform은-멱등성-idempotent-을-갖습니다","children":[]}]},{"level":2,"title":"🛫 Change Your Prefix","slug":"🛫-change-your-prefix","link":"#🛫-change-your-prefix","children":[{"level":3,"title":"Terraform은 인프라를 Create, Destroy, Update, re-Create 합니다.","slug":"terraform은-인프라를-create-destroy-update-re-create-합니다","link":"#terraform은-인프라를-create-destroy-update-re-create-합니다","children":[]},{"level":3,"title":"리소스 내용 변경 시","slug":"리소스-내용-변경-시","link":"#리소스-내용-변경-시","children":[]}]},{"level":2,"title":"🛫 Create and Change ACL","slug":"🛫-create-and-change-acl","link":"#🛫-create-and-change-acl","children":[{"level":3,"title":"Terraform은 인프라를 Create, Destroy, Update, re-Create 합니다.","slug":"terraform은-인프라를-create-destroy-update-re-create-합니다-1","link":"#terraform은-인프라를-create-destroy-update-re-create-합니다-1","children":[]},{"level":3,"title":"리소스 내용 변경 시","slug":"리소스-내용-변경-시-1","link":"#리소스-내용-변경-시-1","children":[]},{"level":3,"title":"Terraform은 항상 현재 인프라를 코드에 정의된 것과 일치시키려고 합니다.","slug":"terraform은-항상-현재-인프라를-코드에-정의된-것과-일치시키려고-합니다","link":"#terraform은-항상-현재-인프라를-코드에-정의된-것과-일치시키려고-합니다","children":[]},{"level":3,"title":"Terraform 코드는 한 번에 하나 또는 두 개의 리소스를 사용하여 점진적으로 빌드할 수 있습니다.","slug":"terraform-코드는-한-번에-하나-또는-두-개의-리소스를-사용하여-점진적으로-빌드할-수-있습니다","link":"#terraform-코드는-한-번에-하나-또는-두-개의-리소스를-사용하여-점진적으로-빌드할-수-있습니다","children":[]}]},{"level":2,"title":"🏗️ Complete the Build","slug":"🏗️-complete-the-build","link":"#🏗️-complete-the-build","children":[{"level":3,"title":"-auto-approve 플래그","slug":"auto-approve-플래그","link":"#auto-approve-플래그","children":[]},{"level":3,"title":"사용에 주의가 필요하니다.","slug":"사용에-주의가-필요하니다","link":"#사용에-주의가-필요하니다","children":[]}]},{"level":2,"title":"😱 Quiz Time 3. Terraform Apply","slug":"quiz-time-3-terraform-apply","link":"#quiz-time-3-terraform-apply","children":[]}],"git":{"createdTime":1695042774000,"updatedTime":1698218919000,"contributors":[{"name":"Great-Stone","email":"hahohh@gmail.com","commits":2}]},"readingTime":{"minutes":2.18,"words":653},"filePathRelative":"03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/03-z-lab_terraform_action.md","localizedDate":"2023년 9월 18일","excerpt":"\\nOpen Folder...
를 클릭합니다.lab02
을 열어줍니다.@slidestart blood
\\ndocmoa에 문서 템플릿을 설명합니다.
주의
기본 템플릿 가이드를 잘 지켜주세요. 함께 만드는 문서 모음이므로, 기본적인 형식이 필요합니다.
h1
으로 지정되는(#
) 제목이 기본으로 적용됩니다.meta
에 추가 기입되는 정보입니다. 검색 엔진에 활용됩니다.docmoa에 문서 템플릿을 설명합니다.
\\n주의
\\n기본 템플릿 가이드를 잘 지켜주세요. 함께 만드는 문서 모음이므로, 기본적인 형식이 필요합니다.
\\n---\\n\\n---\\n\\n# h1 제목 = Title 입니다.\\n내용은 마크다운 형식으로 작성합니다.\\n\\n## h2 제목\\n\\n
빌드를 수행하기 위한 Worker로 다중 Jenkins를 컨트롤 할 수 있습니다. 이때 명령을 수행하는 Jenkins는 Master
, 빌드를 수행하는 Jenkins는 Worker
로 구분합니다. 여기서는 Worker의 연결을 원격 호스트의 Jenkins를 SSH를 통해 연결하는 방식과 컨테이너로 구성된 Jenkins를 연결하는 과정을 확인 합니다.
Master-Slave 방식, 또는 Master-Agent 방식으로 표현합니다.
팁
※ Slave 호스트에 Jenkins를 설치할 필요는 없습니다.
Worker가 실행되는 Slave 호스트에 SSH key를 생성하고 Worker 호스트에 인증 키를 복사하는 과정은 다음과 같습니다.
키 생성 및 복사(jenkins 를 수행할 유저를 생성해야 합니다.)
# User가 없는 경우 새로운 Jenkins slave 유저 추가
+$ useradd jenkins
+$ passwd jenkins
+Changing password for user jenkins.
+New password:
+Retype new password:
+
+# Slave 호스트에서 ssh 키를 생성합니다.
+$ ssh-keygen -t rsa
+Generating public/private rsa key pair.
+Enter file in which to save the key (/root/.ssh/id_rsa): <enter>
+Created directory '/root/.ssh'.
+Enter passphrase (empty for no passphrase): <enter>
+Enter same passphrase again: <enter>
+Your identification has been saved in /root/.ssh/id_rsa.
+Your public key has been saved in /root/.ssh/id_rsa.pub.
+The key fingerprint is: <enter>
+SHA256:WFU7MRVViaU1mSmCA5K+5yHfx7X+aV3U6/QtMSUoxug root@jenkinsecho.gyulee.com
+The key's randomart image is:
++---[RSA 2048]----+
+| .... o.+.=*O|
+| .. + . *o=.|
+| . .o. +o. .|
+| . o. + ... +|
+| o.S. . +.|
+| o oE .oo.|
+| = o . . +o=|
+| o . o ..o=|
+| . ..o+ |
++----[SHA256]-----+
+
+$ cd ~/.ssh
+$ cat ./id_rsa.pub > ./authorized_keys
+
Jenkins 관리
의 노드 관리
를 선택합니다.
좌측 메뉴에서 신규 노드
를 클릭합니다.
노드명에 고유한 이름을 입력하고 Permanent Agent
를 활성화 합니다.
새로운 노드에 대한 정보를 기입합니다.
Use this node as much as possible
Launch agent agents via SSH
로 설정합니다. ADD > Jenkins
를 클릭합니다.SSH Username with private key
를 선택합니다.~/.ssh/id_rsa
의 내용을 붙여넣어줍니다. (일반적으로 -----BEGIN RSA PRIVATE KEY-----
로 시작하는 내용입니다.)Non verifying verification strategy
를 선택합니다.빌드 실행 상태
에 새로운 Slave Node가 추가됨을 확인 할 수 있습니다.Label 지정한 Slave Worker에서 빌드가 수행되도록 기존 02-02.Jobs의 Pipeline 스크립트를 수정합니다. 기존 agent any
를 다음과 같이 agent { label 'Metal' }
로 변경합니다. 해당 pipeline은 label이 Metal
로 지정된 Worker에서만 빌드를 수행합니다.
pipeline {
+ agent { label 'Metal' }
+ parameters {
+ string(name: 'Greeting', defaultValue: 'Hello', description: 'How should I greet the world?')
+ }
+ stages {
+ stage('Example') {
+ steps {
+ echo "\${params.Greeting} World!"
+ }
+ }
+ }
+}
+
Master Jenkins 호스트에서 docker 서비스에 설정을 추가합니다. docker 설치가 되어있지 않은 경우 설치가 필요합니다.
$ yum -y install docker
+
RHEL8 환경이 Master인 경우 위와 같은 방식으로 설치를 진행하면 변경된 패키지에 따라
podman-docker
가 설치 됩니다. 아직 Jenkins에서는 2019년 7월 29일 기준podman
을 지원하지 않음으로 별도 yum repository를 추가하여 진행합니다.docker-ce
최신 버전에서는containerd.io
의 필요 버전이1.2.2-3
이상이나 RHEL8에서 지원하지 않음으로 별도로 버전을 지정하여 설치합니다.$ yum -y install yum-utils +$ yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo +$ sudo yum repolist -v +... +Repo-id : docker-ce-stable +Repo-name : Docker CE Stable - x86_64 +Repo-revision: 1564098258 +Repo-updated : Fri 26 Jul 2019 08:44:18 AM KST +Repo-pkgs : 47 +Repo-size : 982 M +Repo-baseurl : https://download.docker.com/linux/centos/7/x86_64/stable +Repo-expire : 172,800 second(s) (last: Thu 25 Jul 2019 07:33:33 AM KST) +Repo-filename: /etc/yum.repos.d/docker-ce.repo +... + +$ yum -y install docker-ce-3:18.09.1-3.el7 +$ systemctl enable docker +$ systemctl start docker +
docker를 설치 한 뒤 API를 위한 TCP 포트를 활성화하는 작업을 진행합니다./lib/systemd/system/docker.service
에 ExecStart
옵션 뒤에 다음과 같이 -H tcp://0.0.0.0:4243
을 추가합니다.
...
+[Service]
+Type=notify
+# the default is not to use systemd for cgroups because the delegate issues still
+# exists and systemd currently does not support the cgroup feature set required
+# for containers run by docker
+ExecStart=/usr/bin/dockerd -H fd:// -H tcp://0.0.0.0:4243
+ExecReload=/bin/kill -s HUP $MAINPID
+TimeoutSec=0
+RestartSec=2
+Restart=always
+...
+
수정 후 서비스를 재시작합니다.
$ systemctl daemon-reload
+$ systemctl restart docker
+$ docker ps
+CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
+
+$ usermod -aG docker jenkins
+$ chmod 777 /var/run/docker.sock
+
Jenkins에 새로운 플러그인을 추가하고 설정합니다.
Jenkins 관리
로 이동하여 플러그인 관리
를 클릭합니다.
설치 가능
탭을 클릭하고 상단의 검색에 docker
를 입력하면 docker
플러그인이 나타납니다. 선택하여 설치를 진행하고 Jenkins를 재시작 합니다.
Jenkins 관리
로 이동하여 시스템 설정
을 클릭합니다.
Cloud
항목 아래 ADD A NEW CLOUD
드롭박스에 있는 Docker
를 선택합니다.
Name은 기본 값으로 진행하고 DOCKER CLOUD DETAILS...
버튼을 클릭합니다.
Docker Host URI : 앞서 설정한 port로 연결합니다. (e.g. tcp://master:4243)
TEST CONNECTION
버튼을 눌러 정상적으로 Version 정보와 API Version이 표기되는지 확인합니다.
Version = 18.09.1, API Version = 1.39
+
Enabled를 활성화 합니다.
Docker 실행을 위한 Item을 생성합니다. (e.g. 04-02.UsingDockerImagesForAgents)
Pipeline
스크립트를 구성합니다.pipeline {
+ agent {
+ docker { image 'node:latest' }
+ }
+ stages {
+ stage('Test'){
+ steps {
+ sh 'node --version'
+ }
+ }
+ }
+}
+
수정 후 좌측 Build Now
를 클릭하여 빌드 수행 후 결과를 확인합니다.
Step 1
에서의 결과와는 달리 Stage View
항목과 Pipeline stage가 수행된 결과를 확인할 수 있는 UI가 생성됩니다.
Freestyle project
형태의 Item을 생성합니다. (e.g. 04-03.ConfiguringSpecificAgents)
Jenkins는 각 단계, 빌드, 그리고 빌드 후 작업일 지정할 수 있습니다.
Freestyle project
에서는 이같은 전체 빌드 단계를 구성하고 여러가지 플러그인을 사용할 수 있는 환경을 제공합니다.
General
Restrict where this project can be run : 빌드 수행을 특정 Label 노드에 제한하도록 설정할 수 있습니다.
Label Expression : 앞서의 과정에서 생성한 노드 Metal
을 지정해봅니다. 해당 조건의 노드가 존재하는 경우 노드 개수 정보가 표기됩니다.
Label Metal is serviced by 1 node.
+
Build
ADD BUILD STEP
드롭박스에서 Excute shell
항목을 선택하여 추가 합니다. echo "Hello world."
를 넣어봅니다.ADD BUILD STEP
드롭박스에서 Excute shell
항목을 선택하여 추가 합니다. ls -al"
를 넣어봅니다.저장하고 좌측의 Build Now
를 클릭하여 빌드를 수행합니다.
콘솔 출력을 확인하면 지정한 Label 노드에서 각 빌드 절차가 수행된 것을 확인할 수 있습니다.
빌드를 수행하기 위한 Worker로 다중 Jenkins를 컨트롤 할 수 있습니다. 이때 명령을 수행하는 Jenkins는 Master
, 빌드를 수행하는 Jenkins는 Worker
로 구분합니다. 여기서는 Worker의 연결을 원격 호스트의 Jenkins를 SSH를 통해 연결하는 방식과 컨테이너로 구성된 Jenkins를 연결하는 과정을 확인 합니다.
Master-Slave 방식, 또는 Master-Agent 방식으로 표현합니다.
\\n팁
\\n※ Slave 호스트에 Jenkins를 설치할 필요는 없습니다.
\\n리스너는 Tcp로 활성화되는 HTTP 프로토콜을 상징하여 설명합니다. 톰캣은 기본적으로 HTTP, AJP, Shutdown 을 위한 port가 활성화 됩니다. 리스너는 우리 몸의 귀와 같은 역할을 합니다. 들려오는 요청을 받는 역할을 하지요. 톰캣 또한 요청을 받아들이기 위해 리스너를 활성화하여 요청을 받아 들입니다.
이러한 리스너는 톰캣의 "Coyote" 컴포넌트가 담당하는데 톰캣의 Startup.sh(bat)
을 수행하면 다음과 같은 메시지를 확인할 수 있습니다.
7월 15, 2014 5:46:18 오후 org.apache.coyote.AbstractProtocol start 정보: Starting ProtocolHandler ["http-bio-8080"]
+7월 15, 2014 5:46:18 오후 org.apache.coyote.AbstractProtocol start 정보: Starting ProtocolHandler ["ajp-bio-8009"]
+7월 15, 2014 5:46:18 오후 org.apache.catalina.startup.Catalina start 정보: Server startup in 1002 ms
+
http에 8080포트가 할당되고 ajp에 8009포트가 할당됩니다. 이런 포트로 톰캣에 HTTP 혹은 AJP로 요청을 전달할 수 있습니다. 만약 톰캣이 기동된 서버의 IP가 "192.168.0.10"이고 사용되는 HTTP 포트가 8080 이라면
http://192.168.0.10:8080
이렇게 브라우저에서 호출이 가능합니다. 해당 IP가 DNS나 별도의 호스팅 서비스를 통해 www.tomcat-gm.com
에 연결되어 있다면
http://www.tomcat-gm.com:8080
이렇게 호출이 가능합니다.
일반적으로는 HTTP의 기본 포트로 80이 사용되고 HTTPS(SSL)의 기본 포트로 443이 사용됩니다. 이경우 별도의 포트 지정없이 url 요청이 가능합니다.
http://www.tomcat-gm.com(:80)
https://www.tomcat-gm.com(:443)
이러한 리스너 설정은 톰캣의 설정에서 Connector
로 정의됩니다. $CATALINA_HOME/conf
디렉토리의 server.xml
을 열어보시면 다음과 같은 Connector 설정을 확인 할 수 있습니다.
<Connector port="8080" protocol="HTTP/1.1"
+ connectionTimeout="20000"
+ minSpareThreads="10"
+ maxSpareThreads="5"
+ maxThreads="15"
+ redirectPort="8443" />
+
프로토콜의 형태가 "HTTP/1.1"이고 포트는 "8080"으로 활성화 됩니다. 해당 플랫폼에 IP가 여러개 존재한다면 "address" 항목을 추가하여 별도의 IP를 지정 할 수도 있습니다. 이같은 설정은 AJP나 SSL 또한 마찬가지 입니다.
Java의 장점이 무엇인가 물으면 그 대표적인 한가지는 OS 플랫폼에 종속적이지 않은 어플리케이션 개발이 가능하다 일 것입니다. 이런 개발 환경이 가능한 이유는 JVM(Java Virtual Machine)이 제공하는 환경 때문입니다. JVM이 동작하면 각 OS에 Java가 공통적으로 수행되기 위한 Runtime 환경을 만들고 이후 생성된 JVM 환경에서 어플리케이션이 수행되기 때문에 OS 플랫폼 마다 개발을 달리하지 않아도 됩니다. 하지만 각각의 플랫폼에서의 JVM은 그 동작의 목적은 같아도 어플리케이션에 따라 성능에 차이가 발생할 수 있습니다. 어떤 어플리케이션은 한번에 큰 메모리를 요구하는가 하면 때로는 계산을 주로 한다던가, IO 작업이 많다던가하여 CPU 자원을 많이 필요로 하는 식이죠. 따라서 JVM에서는 사용자가 조절할 수 있는 수많은 옵션을 제공합니다. 물론 아무것도 설정하지 않은 상태가 가장 일반적일 수는 있지만 성능이나 장애극복을 위해 Java Option이 추가되기도 합니다. 적용되는 Java Option의 예는 다음과 같습니다.
옵션 | 설명 |
---|---|
-Xms(ms) | Heap 메모리의 최소값을 정의합니다. |
-Xmx(mx) | Heap 메모리의 최대값을 정의합니다. |
-verbosegc | JVM에서 수행하는 GC를 로그로 남깁니다. |
-XX:+AggressiveOpts | 소수점 컴파일을 최적화 합니다. |
-Djava.net.preferIPv4Stack=true | IPv4와 IPv6모두 사용할 수 있는 환경에서 IPv4를 우선하여 서비스 합니다. |
이 외에도 수많은 Java Option이 존재하기에 각 환경에 맞는 Java Option의 적용이 필요하겠습니다. 하지만 어플리케이션이 실제 수행되기 전에는 어떤 요구사항이 발생하는지는 알 수 없기 때문에 반드시 실 서비스를 하기 전 충분한 테스트가 필요합니다.
톰캣에서 Java Options의 추가를 위해서는 setenv.sh(bat)
혹은 catalina.sh(bat)
의 스크립트에 추가하는 방법과 Windows 서비스에 등록된 경우 관련 설정창에 추가하는 방법이 있습니다.
(서비스로 등록된 톰캣에 Java Options 적용 예)
Java 환경에서는 class를 호출하여 서비스를 수행합니다. 각 class는 단독으로, 혹은 여러개가 함께 각각의 Class에 정의된 역할을 수행합니다. 이런 Class를 사용하기 위해서는 ClassLoader가 Class를 읽어 Class를 나열하는 과정이 수행됩니다. 나열되는 Class들은 경로의 형태를 띄며 이를 ClassPath라고도 부릅니다.
...:class:class:class:class:...
ClassLoader가 Class를 읽지 못한다면 JVM에서는 해당 Class에 들어있는 Method를 요청할 때 찾지 못하는 상황이 발생하며, 이경우 ClassNotFound
메시지를 발생시킵니다.
또한 이렇게 정의되는 ClassPath에는 우선순위가 있습니다. 동일한 Class명의 동일한 Method이지만 다른 역할을 수행하는 Class가 로딩된다면 어떤것이 우선권을 갖을까요? ClassPath순서상 앞서있는 Class가 우선권을 갖습니다. 아래와 같은 ClassPath가 생성되었다면 classA가 우선권을 갖습니다.
...:classA:classB:classC:classE:...
그렇다면 어플리케이션 개발자가 의도한 Class를 호출하기 위해서는 ClassLoader가 원하는 class를 앞서 설정하도록 해야합니다. 물론 겹치는 Class가 없다는 가정하에는 어떤 위치에 있던지 상관없이 읽히기만 하면 되겠지요.
일반적으로 웹 어플리케이션을 위한 war
형태의 애플리케이션 개발시 Class는 WEB-INF/classes
jar형태의 라이브러리는 WEB-INF/lib
에 위치시킵니다. 이렇게 위치된 Class들은 톰캣 혹은 WAS에 배치(deploy)되면 전체 JVM의 가장 뒤에 위치하게 됩니다. 간혹 웹 어플리케이션 형태가 아닌 Class나 라이브러리를 적용하기위해서는 CLASSPATH
라는 변수를 사용하여 ClassLoader가 읽을 수 있도록 합니다. 해당 변수는 WAS마다 상이할 수 있습니다.
실행되는 JVM에서의 ClassLoader 순서를 보면 다음과 같습니다.
BootClassPath:ExtensionClassPath:ClassPath
BootClassPath와 ExtensionClassPath는 Java의 기본 라이브러리를 로딩합니다. rt.jar와 같은 필수 라이브러리가 그 예입니다. 만약 기존 JVM을 hooking하는 식의 클래스를 사용하는 경우에는 이보다 앞서 클래스를 위치시킬 필요가 있습니다. HelloWorld라는 클래서를 BootClassPath앞에 위치하게 하려면 -Xbootclasspath/p:HelloWorld
, 뒤에 적용하려면 -Xbotclasspath/a:HelloWorld
형태를 사용하여 적용합니다. p와 a에 주의합니다. 그리고 일반적인 Class가 위치하는 ClassPath에 위치하게 하기 위해서는 톰켓의 경우 스크립트에 'CLASSPATH'변수를 치환합니다. 그 예는 다음과 같습니다.
export CLASSPATH=HelloWorld
+export CLASSPATH=\${CLASSPATH}:HelloWorld
+export CLASSPATH=HelloWorld:\${CLASSPATH}
+
기 적용된 CLASSPATH가 있는가에 따라 적용하고자하는 Class 혹은 라이브러리 앞, 뒤에 기존 CLASSPATH를 넣어줄 수도 있습니다.
경고
Windows의 경우 구분자가 세미콜론(;)이고 그 외에는 콜론(:)임에 주의합니다.
Windows 환경의 서비스 실행방법을 제외하고는 대부분 스크립트에 앞서 설명한 Java Option이나 ClassPath를 설정합니다. 일반적으로, 그리고 여러 운영환경에서 이러한 실행 환경 변수를 catalina.sh(bat)
에 설정하여 사용하는 경우를 보았습니다. 하지만 한번이라도 해당 스크립트를 열어 읽어보셨다면 다음과 같은 메시지를 확인 할 수 있을 것입니다.
# -----------------------------------------------------------------------------
+# Control Script for the CATALINA Server
+#
+# Environment Variable Prerequisites
+#
+# Do not set the variables in this script. Instead put them into a script
+# setenv.sh in CATALINA_BASE/bin to keep your customizations separate.
+
즉, catalina.sh
는 건드리지 말고 setenv.sh(bat)
에 추가적은 설정을 하라는 안내 문구 입니다. catalina.sh
를 수정하는 경우 해당 톰캣을 이전하거나, 동일한 톰캣을 구성하거나, 또는 복구해야 하는 상황에서 추가로 설정되거나 수정된 내용의 확인이 힘들 수 있고, 또한 설정과 수정으로 인한 비정상 동작을 할 수 있기 때문입니다. 그렇다면 setenv.sh(bat)
은 어떻게 작용할까요? catalina.sh(bat)
에서 setenv
를 찾아보면 다음과 같이 setenv
스크립트에 적용된내용을 읽어오는 것을 확인 할 수 있습니다.
이같이 톰캣에서는 추가/수정해야하는 환경변수나 설정값을 하나의 스크립트에서 관리할 수 있는 환경을 제공합니다. 다만 setenv.sh(bat)
스크립트는 별도로 만들어야 합니다. 앞서 catalina.sh(bat)
의 설명된 변수들을 보면 Java Options은 JAVA_OPTS
로하지 말고 CATALINA_OPTS
로 하라는 점도 주의해서 보시기 바랍니다. JAVA_OPTS
의 경우 톰캣을 정지시키는 shutdown.sh(bat)
에서도 호출되기 때문에 차후 소개되는 JMX 모니터링을 위한 옵션과 같이 별도의 포트를 활성화하는 옵션과 같은 성격의 설정 적용시 문제가 될 수 있습니다. setenv.sh(bat)
스크립트에 다음과 같이 추가하면 해당 옵션을 별도로 export하지 않아도 톰캣 기동시 적용됩니다.
웹 어플리케이션에서 web.xml
은 서블릿을 정의하고 이어주는 역할을 수행합니다. 이와 마찬가지로 톰캣에 있는 $CATALINA_HOME/conf/web.xml
또한 톰캣에 있는 서블릿을 정의하고 이어주는 역할을 수행합니다. 다만 JSP/Servlet 엔진으로서의 최소한의 정의를 합니다.
따라서 톰캣에 배치되는 모든 어플리케이션에서 공통으로 수정되어야 할 사항은 web.xml에도 정의할 수 있습니다. 하지만 앞서 catalina
스크립트와 마찬가지로 추가/수정시 부작용이 있을 수 있음에 중의합니다.
톰캣의 로그는 다음과 같이 종류와 정의는 다음에서 정의합니다. 다만 Windows 서비스는 서비스 환경설정에서 정의힙니다.
Log | Config File |
---|---|
Catalina.out | CATALINA_OUT 환경변수로 정의, catalina.sh에 정의되어 있고 setenv 스크립트에서 재정의 |
access.log | server.xml |
*.log | logging.properties |
Terraform 파일 프로비저닝 도구는 원격 시스템에 파일을 복사합니다.
provisioner "file" {
+ source = "files/"
+ destination = "/home/${var.admin_username}/"
+ connection {
+ type = "ssh"
+ user = var.username
+ private_key = file(var.ssh_key)
+ host = ${self.ip}
+ }
+}
+
provisioner 블록 안에있는 코드의 connection 블록에 주목하세요. 파일 프로비저닝 도구는 SSH
, WinRM
연결을 모두 지원합니다.
Remote Exec Provisioner
를 사용하면 대상 호스트에서 스크립트 또는 기타 프로그램을 실행할 수 있습니다.
자동으로 실행할 수있는 경우 (예 : 소프트웨어 설치 프로그램) remote-exec
로 실행할 수 있습니다.
provisioner "remote-exec" {
+ inline = [
+ "sudo chown -R ${var.admin_username}:${var.admin_username} /var/www/html",
+ "chmod +x *.sh",
+ "PLACEHOLDER=${var.placeholder} WIDTH=${var.width} HEIGHT=${var.height} PREFIX=${var.prefix} ./deploy_app.sh",
+ ]
+...
+}
+
이 예에서는 일부 권한 및 소유권을 변경하고 일부 환경 변수가있는 스크립트를 실행하기 위해 몇 가지 명령을 실행합니다.
Terraform은 Chef, Puppet, Ansible과 같은 일반적인 구성 관리 도구와 잘 작동합니다.
',12),b=n("br",null,null,-1),w={href:"https://www.terraform.io/docs/provisioners/chef.html",target:"_blank",rel:"noopener noreferrer"},y=n("br",null,null,-1),x={href:"https://www.terraform.io/docs/provisioners/local-exec.html",target:"_blank",rel:"noopener noreferrer"},T=n("br",null,null,-1),P={href:"https://github.com/scarolan/ansible-terraform",target:"_blank",rel:"noopener noreferrer"},C=n("h2",{id:"terraform-provisioner에-대한-도움말",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#terraform-provisioner에-대한-도움말"},[n("span",null,"Terraform Provisioner에 대한 도움말")])],-1),q=n("p",null,[n("code",null,"remote-exec"),e("와 같은 Terraform 프로비저닝 도구는 몇 가지 간단한 명령이나 스크립트를 실행해야 할 때 유용합니다. 더 복잡한 구성 관리의 경우 Chef 또는 Ansible과 같은 도구가 필요합니다.")],-1),R=n("p",null,[e("Provisioner는 Terraform 실행이 "),n("mark",null,"처음 실행될 때"),e(" 만 실행됩니다. 이러한 의미에서 그 동작들은 멱등성을 띄지 않습니다.")],-1),N=n("p",null,"수명이 긴 VM 또는 서버의 지속적인 상태 관리가 필요한 경우 이같은 구성 관리 도구를 활용할 수 있습니다.",-1),E={href:"https://www.packer.io/",target:"_blank",rel:"noopener noreferrer"},A=n("iframe",{width:"560",height:"315",src:"https://www.youtube.com/embed/Dqwk7fYHhVQ",title:"YouTube video player",frameborder:"0",allow:"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture",allowfullscreen:""},null,-1),$=n("hr",null,null,-1),L=n("p",null,"실습을 위해 다음장으로 이동하세요.",-1);function V(B,H){const o=s("ExternalLinkIcon"),t=s("RouteLink");return p(),i("div",null,[m,h,k,f,v,n("p",null,[n("a",_,[e("https://www.terraform.io/docs/provisioners/index.html"),a(o)])]),g,n("ul",null,[n("li",null,[n("p",null,[e("Official Chef Terraform provisioner:"),b,n("a",w,[e("https://www.terraform.io/docs/provisioners/chef.html"),a(o)])])]),n("li",null,[n("p",null,[e("Run Puppet with 'local-exec':"),y,n("a",x,[e("https://www.terraform.io/docs/provisioners/local-exec.html"),a(o)])])]),n("li",null,[n("p",null,[e("Terraform and Ansible - Better Together:"),T,n("a",P,[e("https://github.com/scarolan/ansible-terraform"),a(o)])])])]),C,q,R,N,n("p",null,[e("반면에 변경 불가능한 인프라를 원하면 "),n("a",E,[e("Packer"),a(o)]),e(" 같은 이뮤터블을 위한 빌드 도구를 사용하는 것이 좋습니다.")]),A,$,L,n("p",null,[a(t,{to:"/03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/04-z-lab_provisioning_and_configuration.html"},{default:l(()=>[e("💻 Lab - Provisioners, Variables, Outputs")]),_:1})])])}const I=r(d,[["render",V],["__file","04-ncp-provisioning-and-configuration.html.vue"]]),M=JSON.parse('{"path":"/03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/04-ncp-provisioning-and-configuration.html","title":"04. 테라폼 프로비저닝 도구 사용 및 구성","lang":"ko-KR","frontmatter":{"description":"Naver Cloud Platform에서의 Terraform 실습","tag":["ncloud","ncp","terraform","workshop"],"head":[["meta",{"property":"og:url","content":"https://docmoa.github.io/03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/04-ncp-provisioning-and-configuration.html"}],["meta",{"property":"og:site_name","content":"docmoa"}],["meta",{"property":"og:title","content":"04. 테라폼 프로비저닝 도구 사용 및 구성"}],["meta",{"property":"og:description","content":"Naver Cloud Platform에서의 Terraform 실습"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"ko-KR"}],["meta",{"property":"og:updated_time","content":"2023-09-18T13:12:54.000Z"}],["meta",{"property":"article:tag","content":"ncloud"}],["meta",{"property":"article:tag","content":"ncp"}],["meta",{"property":"article:tag","content":"terraform"}],["meta",{"property":"article:tag","content":"workshop"}],["meta",{"property":"article:modified_time","content":"2023-09-18T13:12:54.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"04. 테라폼 프로비저닝 도구 사용 및 구성\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2023-09-18T13:12:54.000Z\\",\\"author\\":[]}"]]},"headers":[{"level":2,"title":"Terraform 프로비저닝 도구 사용","slug":"terraform-프로비저닝-도구-사용","link":"#terraform-프로비저닝-도구-사용","children":[]},{"level":2,"title":"File Provisioner","slug":"file-provisioner","link":"#file-provisioner","children":[]},{"level":2,"title":"Remote Exec Provisioner","slug":"remote-exec-provisioner","link":"#remote-exec-provisioner","children":[]},{"level":2,"title":"Terraform & Config Management Tools","slug":"terraform-config-management-tools","link":"#terraform-config-management-tools","children":[]},{"level":2,"title":"Terraform Provisioner에 대한 도움말","slug":"terraform-provisioner에-대한-도움말","link":"#terraform-provisioner에-대한-도움말","children":[]}],"git":{"createdTime":1695042774000,"updatedTime":1695042774000,"contributors":[{"name":"Great-Stone","email":"hahohh@gmail.com","commits":1}]},"readingTime":{"minutes":0.59,"words":177},"filePathRelative":"03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/04-ncp-provisioning-and-configuration.md","localizedDate":"2023년 9월 18일","excerpt":"\\nTerraform을 사용하여 가상 머신 또는 컨테이너를 세우고 나면 운영 체제와 애플리케이션을 구성 할 수 있습니다.
\\n여기에서 Provisioner 가 등장합니다.
\\nTerraform은 Bash, Powershell, Chef, Puppet, Ansible 등을 포함한 여러 유형의 Provisioner를 지원합니다.
\\nhttps://www.terraform.io/docs/provisioners/index.html
"}');export{I as comp,M as data}; diff --git a/assets/04-traffic-management.html-D79x_2QV.js b/assets/04-traffic-management.html-D79x_2QV.js new file mode 100644 index 0000000000..e3753c2a28 --- /dev/null +++ b/assets/04-traffic-management.html-D79x_2QV.js @@ -0,0 +1,270 @@ +import{_ as l}from"./plugin-vue_export-helper-DlAUqK2U.js";import{r as t,o as d,c as r,b as e,d as n,a,e as i}from"./app-Bzk8Nrll.js";const c="/assets/03-traffic-v1-BGAizMHc.png",v="/assets/03-traffic-v2-CgPD7up9.png",o="/assets/03-traffic-methods-BH17zSFz.png",u="/assets/03-traffic-metadata-C8cC4nAb.png",p="/assets/03-traffic-routeui-Dm1u6sgH.png",m={},b=i(`실습을 진행하기 위한 디렉토리를 생성합니다.
mkdir ./traffic
+
Service Mesh는 HTTP 프로토콜 상에서 L7으로 동작하게 됩니다. 따라서 기본 프로토콜을 http로 변경합니다.
cat > ./traffic/service-to-service.yaml <<EOF
+apiVersion: consul.hashicorp.com/v1alpha1
+kind: ProxyDefaults
+metadata:
+ name: global
+spec:
+ config:
+ protocol: http
+EOF
+
kubectl apply -f ./traffic/service-to-service.yaml
+
# 출력
+proxydefaults.consul.hashicorp.com/global created
+
cat > ./traffic/gs-frontend.yaml <<EOF
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: gs-frontend
+spec:
+ selector:
+ app: gs-frontend
+ ports:
+ - protocol: TCP
+ port: 3000
+ targetPort: 3000
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: gs-frontend
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: gs-frontend
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: gs-frontend
+ template:
+ metadata:
+ labels:
+ app: gs-frontend
+ annotations:
+ prometheus.io/scrape: "true"
+ prometheus.io/port: "9901"
+ consul.hashicorp.com/connect-inject: "true"
+ consul.hashicorp.com/transparent-proxy: true
+ consul.hashicorp.com/connect-service-upstreams: "gs-backend:8080"
+ spec:
+ serviceAccountName: gs-frontend
+ containers:
+ - name: gs-frontend
+ image: hahohh/consul-frontend-nodejs:v1.5
+ env:
+ - name: PORT
+ value: "3000"
+ - name: UPSTREAM_URL
+ value: "http://localhost:8080
+ ports:
+ - containerPort: 3000
+EOF
+
적용하기
kubectl apply -f ./traffic/gs-frontend.yaml
+
cat > ./traffic/gs-backend.yaml <<EOF
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: gs-backend
+spec:
+ selector:
+ app: gs-backend
+ ports:
+ - protocol: TCP
+ port: 8080
+ targetPort: 8080
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: gs-backend
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: gs-backend-v1
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: gs-backend
+ version: v1
+ template:
+ metadata:
+ labels:
+ app: gs-backend
+ version: v1
+ annotations:
+ consul.hashicorp.com/connect-inject: "true"
+ consul.hashicorp.com/service-meta-version: v1
+ consul.hashicorp.com/service-tags: v1
+ spec:
+ serviceAccountName: gs-backend
+ containers:
+ - name: gs-backend
+ image: hahohh/consul-backend-go:v1.2
+ env:
+ - name: PORT
+ value: "8080"
+ - name: COLOR
+ value: "green"
+ - name: VERSION
+ value: "v1"
+ ports:
+ - containerPort: 8080
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: gs-backend-v2
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: gs-backend
+ version: v2
+ template:
+ metadata:
+ labels:
+ app: gs-backend
+ version: v2
+ annotations:
+ consul.hashicorp.com/connect-inject: "true"
+ consul.hashicorp.com/service-meta-version: v2
+ consul.hashicorp.com/service-tags: v2
+ spec:
+ serviceAccountName: gs-backend
+ containers:
+ - name: gs-backend
+ image: hahohh/consul-backend-go:v1.2
+ env:
+ - name: PORT
+ value: "8080"
+ - name: COLOR
+ value: "blue"
+ - name: VERSION
+ value: "v2"
+ # - name: ISFAIL
+ # value: "yyyy"
+ ports:
+ - containerPort: 8080
+EOF
+
적용하기
kubectl apply -f ./traffic/gs-backend.yaml
+
cat > ./traffic/service-to-service.yaml <<EOF
+apiVersion: consul.hashicorp.com/v1alpha1
+kind: ServiceIntentions
+metadata:
+ name: gs-backend
+spec:
+ destination:
+ name: gs-backend
+ sources:
+ - name: gs-frontend
+ action: allow
+EOF
+
적용하기
kubectl apply -f ./traffic/service-to-service.yaml
+
port-forward
를 통해 로컬에서 web 앱을 확인합니다.
kubect l port-forward service/gs-frontend 3000:3000 --address 0.0.0.0
+
# 출력
+Forwarding from 0.0.0.0:3000 -> 3000
+
두개의 버전의 백엔드가 서로다른 갑을 리턴하여 때에 따라 v1, v2가 번갈아 나타납니다.
cat > ./traffic/service-resolver.yaml <<EOF
+apiVersion: consul.hashicorp.com/v1alpha1
+kind: ServiceResolver
+metadata:
+ name: gs-backend
+spec:
+ defaultSubset: v1
+ subsets:
+ v1:
+ filter: "Service.Meta.version == v1"
+ v2:
+ filter: "Service.Meta.version == v2"
+EOF
+
적용하기
kubectl apply -f ./traffic/service-resolver.yaml
+
앞서 배포된 gs-backend
의 Deployment
에 선언된 annotation
내용을 확인하면 consul.hashicorp.com/service-meta-version: v2
을 확인할 수 있습니다. Consul UI에서도 해당 Meta 정보를 확인할 수 있습니다. 선언된 정보를 기반으로 서비스의 subset
을 정의합니다.
cat > ./traffic/service-splitter.yaml <<EOF
+apiVersion: consul.hashicorp.com/v1alpha1
+kind: ServiceSplitter
+metadata:
+ name: gs-backend
+spec:
+ splits:
+ - weight: 50
+ serviceSubset: v1
+ - weight: 50
+ serviceSubset: v2
+EOF
+
적용하기
kubectl apply -f ./traffic/service-splitter.yaml
+
weight
에 지정된 비율로 Resolve된 서비스 대상 subset
에 트래픽을 분산합니다. weight
값을 0:100, 100:0 등으로 변경하여 요청의 결과가 어떻게 변화하는지 확인해 봅니다.
cat > ./traffic/service-router.yaml <<EOF
+apiVersion: consul.hashicorp.com/v1alpha1
+kind: ServiceRouter
+metadata:
+ name: gs-backend
+spec:
+ routes:
+ - match:
+ http:
+ pathPrefix: '/v1'
+ destination:
+ service: gs-backend
+ serviceSubset: v1
+ - match:
+ http:
+ pathPrefix: '/v2'
+ destination:
+ service: gs-backend
+ serviceSubset: v2
+ - match:
+ http:
+ queryParam:
+ - name: version
+ exact: 'v1'
+ destination:
+ service: gs-backend
+ serviceSubset: v1
+ - match:
+ http:
+ queryParam:
+ - name: version
+ exact: 'v2'
+ destination:
+ service: gs-backend
+ serviceSubset: v2
+EOF
+
적용하기
kubectl apply -f ./traffic/service-router.yaml
+
예제에서는 url의 path, queryParam을 예로 트래픽을 컨트롤 합니다. 다음과같이 요청하여 트래픽이 조정되는 것을 확인합니다.
`,5),y={href:"http://localhost:3000/v1",target:"_blank",rel:"noopener noreferrer"},x={href:"http://localhost:3000/v2",target:"_blank",rel:"noopener noreferrer"},_={href:"http://localhost:3000/?version=v1",target:"_blank",rel:"noopener noreferrer"},S={href:"http://localhost:3000/?version=v2",target:"_blank",rel:"noopener noreferrer"},E=i(`service-to-service 허용 방식에도 Meshod, Path 등을 지정할 수 있습니다. 다음과 같이 변경하고 POST만 넣은 상태에서는 어떻게 동작하는지 확인합니다.
cat > ./traffic/service-to-service.yaml <<EOF
+apiVersion: consul.hashicorp.com/v1alpha1
+kind: ServiceIntentions
+metadata:
+ name: gs-backend
+spec:
+ destination:
+ name: gs-backend
+ sources:
+ - name: gs-frontend
+ permissions:
+ - action: allow
+ http:
+ pathPrefix: /
+ # methods: ['GET', 'PUT', 'POST', 'DELETE', 'HEAD']
+ methods: ['POST']
+
적용하기
kubectl apply -f ./traffic/service-to-service.yaml
+
다시 앱간의 요청인 GET
으로 변경하고 트래픽 허용여부를 확인해봅니다.
cat > ./traffic/service-to-service.yaml <<EOF
+apiVersion: consul.hashicorp.com/v1alpha1
+kind: ServiceIntentions
+metadata:
+ name: gs-backend
+spec:
+ destination:
+ name: gs-backend
+ sources:
+ - name: gs-frontend
+ permissions:
+ - action: allow
+ http:
+ pathPrefix: /
+ # methods: ['GET', 'PUT', 'POST', 'DELETE', 'HEAD']
+ methods: ['GET']
+
적용하기
kubectl apply -f ./traffic/service-to-service.yaml
+
Consul UI에 접속하여 gs-backend
의 Routing
탭을 클릭, 구성된 Resolver, Splitter, Router가 어떻게 구성되었는지, 각 서비스에는 어떤 조건으로 요청할 수 있는지 확인합니다.
실습을 진행하기 위한 디렉토리를 생성합니다.
\\nmkdir ./traffic\\n
Service Mesh는 HTTP 프로토콜 상에서 L7으로 동작하게 됩니다. 따라서 기본 프로토콜을 http로 변경합니다.
\\ncat > ./traffic/service-to-service.yaml <<EOF\\napiVersion: consul.hashicorp.com/v1alpha1\\nkind: ProxyDefaults\\nmetadata:\\n name: global\\nspec:\\n config:\\n protocol: http\\nEOF\\n
Open Folder...
를 클릭합니다.lab02
을 열어줍니다.@slidestart blood
terraform apply
를 입력할 때마다 프로비저닝 도구가 강제로 실행되도록 몇 가지 특별한 조정을 했습니다.이는 변경할 때마다 가상 머신을 파괴하고 다시 생성하지 않고 프로비저닝 도구를 사용하여 연습할 수 있도록 하기 위한 것입니다.
triggers = {
+ build_number = timestamp()
+}
+
______________________
+< Cows love Terraform! >
+ ----------------------
+ \\ ^__^
+ \\ (oo)\\_______
+ (__)\\ )\\/\\
+ ||----w |
+ || ||
+=============================
+
@slideend
main.tf
파일을 열어 remote-exec
항목이 있는 곳으로 이동합니다.
inline
항목에 다음을 두줄 추가합니다.
"sudo apt -y install cowsay",
+"cowsay Mooooooooooo!",
+
팁
terraform fmt
명령을 사용하여 코드를 멋지게 정렬 할 수 있는 좋은 시간 입니다.
이제 변경 사항을 적용하십시오.
terraform apply -auto-approve
+
로그 출력을 뒤로 스크롤합니다. "Moooooooo!"라고 말하는 ASCII 아트 암소가 보일 것입니다.
@slidestart blood
출력은 실행이 끝날 때 사용자에게 유용한 정보를 전달하는 데 사용할 수 있습니다.
terraform refresh
명령은 상태 파일을 인프라에 있는 파일과 동기화합니다.이 명령은 인프라를 변경하지 않습니다.
terraform output
명령을 실행할 수 있습니다.단일 출력을 보려면 terraform output <output_name>
을 실행합니다.
@slideend
output.tf
파일을 열어 아래 항목을 추가합니다.
output "ssh_info" {
+ value = nonsensitive("sshpass -p '${data.ncloud_root_password.hashicat.root_password}' ssh root@${ncloud_public_ip.hashicat.public_ip} -oStrictHostKeyChecking=no")
+}
+
해당 output의 이름은 ssh_info
입니다.
어떤 유형의 출력이 유효한지 보려면 문서 페이지를 참조하세요.
`,43),h={href:"https://registry.terraform.io/providers/NaverCloudPlatform/ncloud/latest/docs/data-sources/root_password#argument-reference",target:"_blank",rel:"noopener noreferrer"},m={href:"https://registry.terraform.io/providers/NaverCloudPlatform/ncloud/latest/docs/resources/public_ip#attributes-reference",target:"_blank",rel:"noopener noreferrer"},f=s(`output.tf
에 새로운 내용을 저장하고 terraform refresh
명령을 실행하여 새로운 출력을 확인합니다.
terraform refresh
+
terraform output
명령을 실행하여 모든 출력을 볼 수도 있습니다.
terraform output
+
@slidestart blood
placeholder
변수로 시도할 수 있는 다른 재미있는 사이트입니다.@slideend
Terraform 변수를 구성하는 방법에는 여러 가지가 있습니다. 지금까지 terraform.tfvars
파일을 사용하여 변수를 설정했습니다.
명령줄에서 기본값과 다른 height
, width
변수를 사용 하여 애플리케이션을 다시 배포해 보십시오.
변경 사항을 관찰하기 위해 적용할 때마다 웹 앱을 다시 로드합니다.
terraform apply -auto-approve -var height=600 -var width=800
+
다음으로 Terraform이 읽을 수 있는 환경 변수를 설정해 보십시오. 다음 명령을 실행하여 placeholder
변수를 설정합니다.
export TF_VAR_placeholder=placedog.net
+
환경 변수 적용 후 terraform apply -auto-approve
를 실행하여 다시 적용해 봅니다.
terraform apply -auto-approve
+
이제 명령줄에서 동일한 변수를 다르게 설정하여 다시 시도하십시오.
terraform apply -auto-approve -var placeholder=placebear.com
+
어떤 변수가 우선시 되었습니까? 잘 이해 되셨나요?
`,13),P={href:"https://www.terraform.io/docs/language/values/variables.html#variable-definition-precedence",target:"_blank",rel:"noopener noreferrer"},C=s('Q. *.tfvars
파일과 환경 변수에 동일한 변수가 설정되어 있습니다. 어느 것이 우선합니까?
이 장에서 우리는 :
file
과 remote-exec
프로비저닝 도구에 대해 알아보았습니다.Open Folder...
를 클릭합니다.lab02
을 열어줍니다.@slidestart blood
\\n톰캣에 배치되는 어플리케이션은 Java Web Application입니다. 간단히 웹 어플리케이션이라고도 합니다. 간략한 구조는 다음과 같습니다.
파일 구조
./APPDIR
+├── WEB-INF
+│ ├── classes
+│ │ └── class-files
+│ ├── lib
+│ │ └── jar-files
+│ └── web.xml
+├── index.html
+└── index.jsp
+
APP 디렉토리 하위에는 웹어플리케이션의 정의를 넣을 WEB-INF 디렉토리가 필요합니다. 아주 간단한 어플리케이션은 web.xml
에 다음의 태그만 넣어도 웹 어플리케이션으로 인지할 수 있습니다.
<web-app/>
+
어플리케이션을 배치하는 방법에는 톰캣에서 제공하는 manager
를 사용하는 방법이 있습니다. manager
는 톰캣을 설치하면 제공되는 어플리케이션 관리 툴로 다음과 같이 확인할 수 있습니다.
<tomcat-users>
+ <role rolename="manager-gui"/>
+ <user username="admin" password="admin" roles="manager-gui"/>
+</tomcat-users>
+
manager의 중간에 Deploy
에서 배치를 수행할 수 있으며 두가지 타입이 제공됩니다. 한가지는 Deploy directory or WAR file located on server
로서 톰캣이 기동된 서버내의 어플리케이션을 지정하여 배치하는 방법과 WAR file to deploy
는 현재 접속중인 로컬의 WAR파일을 업로드하여 배치하는 방법입니다. 두 방법 모두 수행 후 $CATALINA_HOME/webapps
에 해당 어플리케이션이 위치하게 됩니다.
톰캣에는 자동으로 어플리케이션을 인지하고 배치하는 디렉토리가 있습니다. 바로 $CATALINA_HOME/webapps
입니다. 해당 경로는 앞서 manager 를 통한 배치시에도 어플리케이션이 위치하게되는 경로인데, manager를 사용하는 방법은 결국 webapps 디렉토리에 어플리케이션을 위치시키는 작업이라고 볼 수 있습니다. 따라서 직접 해당 경로에 어플리케이션을 위치시켜도 동일하게 배치 작업이 발생합니다.
webapps 디렉토리가 자동으로 배치를 수행하는 설정은 server.xml
에서 해당 경로가 배치를 수행하도록 설정되었기 때문입니다.
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
+ <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
+ prefix="localhost_access_log." suffix=".txt"
+ pattern="%h %l %u %t "%r" %s %b" />
+</Host>
+
server.xml
에서 확인 할 수 있는 톰캣의 기본 host 환경인 localhost
에서 webapps 디렉토리를 대상으로 autoDeploy를 수행한다는 설정 내용입니다. 이러한 설정으로 인해 자동으로 어플리케이션의 배치가 가능합니다.
앞서 두가지의 manager나 webapps 디렉토리를 통한 배치 방법은 모두 톰캣 내부의 webapps 디렉토리에 어플리케이션이 위치하게 된다는 특징이 있습니다. 이러한 점은 사용자가 원하는 임의의 위치에 어플리케이션은 배제 된다는 의미 일 수도 있습니다. 따라서 이경우 context.xml
형태의 xml을 통한 배치 방법을 사용 할 수 있겠습니다.
배치를 설명하기에 앞서 context.xml
의 context
디스크립터는 본래 server.xml에 위치하는 디스크립터였습니다. 하지만 해당 디스크립터에 설정되는 내용들은 변경사항이 자주 발생하는 항목들이기에 별도의 xml에서도 정의할 수 있도록 변경되었으며, 톰캣 5.5.12 버전부터는 server.xml이 아닌 별도의 context.xml을 통하여 해당 설정들을 관리하도록 권고하고 있습니다.
context.xml
에서 설정하는 정보는 어플리케이션 뿐만이 아니기 때문에 어플리케이션 설정을 제외한 설정을 톰캣에 적용하는 경우에도 사용됩니다. context.xml
은 <Context>
디스크립터로 시작되며 다음의 위치에서 적용되고, 위치에 따라 적용 범위가 달라집니다.
이같은 위치에 따른 적용 범위는 context로 정의되는 대표적인 자원중 하나인 데이터소스(DataSource)의 경우 톰캣전체 또는 어플리케이션 별로 구분할 수 있는 기능을 사용할 수 있습니다.
어플리케이션을 배포하는 경우 위의 4가지 방법 중 3, 4번 항목을 들 수 있으며, 특히 context.xml
을 사용한 임의의 위치의 어플리케이션 배포는 3번 항목을 사용하게 됩니다.
sample
context-root를 갖는 어플리케이션은 다음과 같이 context.xml
을 설정할 수 있습니다.
<?xml version="1.0" encoding="UTF-8"?>
+
+<Context path="sample" docBase="/Users/GSLee/APP/sample" debug="0" reloadable="true" crossContext="true" privileged="true"/>
+
+<!-- path는 해당 설정을 server.xml에 하는 경우 의미가 있고 3번 방법의 경우 xml 파일 이름이 context-root로 설정됩니다. -->
+
일반적으로 어플리케이션을 배치하는 경우 해당 어플리케이션의 디렉토리 이름이나 context로 설정된 xml의 파일 이름이 context-root로 사용됩니다.
http://www.mytomcat.co.kr/[WEBAPPNAME]/index.jsp
하지만 대부분의 경우 다음과 같이 요청되기를 바라죠.
http://www.mytomcat.co.kr/index.jsp
이경우 배치 방식은 동일하지만 다음의 네가지 방법을 통해 어플리케이션 배치 시 context-root를 /
로 설정할 수 있습니다.
구성 위치 | 설명 |
---|---|
$CATALINA_HOME/webapps/ROOT | webapps 기본 디렉토리에 ROOT인 디렉토리명으로 배치된 어플리케이션 |
$CATALINA_HOME/conf/[ENGINENAME]/[HOSTNAME]/ROOT.xml | ROOT를 이름으로 갖는 context 타입의 xml로 배치된 어플리케이션 |
Tomcat Manager | APP에서 context path 항목을 비워놓은 채로 배치하는 어플리케이션 |
server.xml에 배치 어플리케이션을 설정 | context 디스크립터의 path 항목을 비워놓음 |
방법은 여러가지가 있지만 앞서 설명드린 context
디스크립터로 설정한 별도의 xml을 사용한 배치 방식을 권장합니다.
sample 어플리케이션을 ROOT로 배치한 로그는 다음과 같이 확인됩니다.
정보: Starting Servlet Engine: Apache Tomcat/8.5.73
+9월 06, 2014 8:30:52 오후 org.apache.catalina.startup.HostConfig deployDescriptor
+정보: Deploying configuration descriptor /Users/GSLee/APP/tomcat/apache-tomcat-8.5.73/conf/Catalina/localhost/ROOT.xml
+9월 06, 2014 8:30:52 오후 org.apache.catalina.core.StandardContext setPath
+경고: A context path must either be an empty string or start with a '/'. The path [sample] does not meet these criteria and has been changed to [/sample]
+9월 06, 2014 8:30:52 오후 org.apache.catalina.startup.SetContextPropertiesRule begin
+경고: [SetContextPropertiesRule]{Context} Setting property 'debug' to '0' did not find a matching property.
+9월 06, 2014 8:30:53 오후 org.rhq.helpers.rtfilter.filter.RtFilter init
+정보: Initialized response-time filter for webapp with context root 'ROOT'.
+9월 06, 2014 8:30:53 오후 org.apache.catalina.startup.HostConfig deployDescriptor
+정보: Deployment of configuration descriptor /Users/GSLee/APP/tomcat/apache-tomcat-8.5.73/conf/Catalina/localhost/ROOT.xml has finished in 1,115 ms
+
Auto Deploy와 Hot Deploy는 Auto와 Hot을 어떻게 해석하는가에 따라 다음과 같이 혼용되어 사용됩니다.
의미가 어떻게 해석되던지 이런 용어를 사용함에 있어서 바라는점은 서비스가 실행중인 도중에도 변경사항을 사용자 모르게 바꾸고자 하는 의도가 대부분일 것입니다.
webapps 디렉토리에 어플리케이션을 넣으면 자동으로 배치가 됩니다. 서버가 기동된 상태에서도 말이죠. 해당 설정은 다음의 'Host' 디스크립터에서 정의합니다.
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
+ ...
+</Host>
+
Host 설정에 autoDeploy가 true
인 경우 해당 디렉토리에 위치하는 어플리케이션을 감지하여 자동으로 톰캣에 배치를 수행합니다.
jsp를 사용자 뷰로 사용하는 경우 서비스의 컨텐츠, 또는 jsp에서 실행하는 코드상의 변경사항이 자주 발생하게 됩니다. 이경우 jsp를 새로 반영하기 위해 서버가 실행중임에도 자동으로 업데이트된 jsp를 컴파일하여 해당 소스를 반영하는 동작을 지원하는 설정이 있습니다. 해당 설정은 다음의 $CATAILNA_HOME/conf/web.xml
의 jsp 서블릿에 정의되어 있습니다.
<servlet>
+ <servlet-name>jsp</servlet-name>
+ <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
+ ...
+ <init-param>
+ <param-name>development</param-name>
+ <param-value>true</param-value>
+ </init-param>
+ <init-param>
+ <param-name>checkInterval</param-name>
+ <param-value>1</param-value>
+ </init-param>
+ <load-on-startup>3</load-on-startup>
+</servlet>
+
jsp 서블릿에서는 두가지 경우에 대해 jsp의 업데이트를 수행합니다.
development
가 true
인 경우 항상 확인development
가 false
이고 checkInterval
이 0
보다 큰 경우 확인합니다.관련 옵션에 대한 상세 내용이나 추가적인 jasper 컴포넌트의 옵션은 해당 설정의 위에 주석으로 설명이 되어있습니다.
클래스를 수정하여 컴파일한 뒤 어플리케이션에 업로드하면 얼마 후 해당 어플리케이션(컨텍스트)를 리로딩하는 과정을 수행합니다. 해당 설정은 Context
디스크립터가 설정된 xml에 정의합니다.
<Context reloadable="true" ...>
+ <Loader checkInterval="15"/>
+</Context>
+
reloadable
을 true
로 설정하는 경우 해당 어플리케이션(컨텍스트)는 클래스에 변경이 발생하면 다시 리로딩하는 기능을 수행합니다. 간격은 기본 15초이기 때문에 더 빠른 방영을 원하시면 Context
디스크립터 내에 Loader
의 checkInterval
을 정의함으로 시간을 조절할 수 있습니다. 하지만 이런 리로드 현상을 싫어하시는 분도 있습니다. "Tomcat Context 수동 Reload"라는 블로그 글에서도 보이듯 자동 리로드를 비활성화 하고 Valve
를 사용하는 별도의 방법을 사용할 수도 있습니다.
그 방법은 WEBAPP##[VersionNumber]
입니다. sample 어플리케이션으로 예를 들면 sample##1.0
으로 배치를 수행하는 것입니다. 기존 어플리케이션 뒤에 샵 기호 두개와 버전이름만 붙이면 되기 때문에 매우 간단하지만 단점으로는 거의 동일한 구성과 용량의 독립적인 어플리케이션이 필요하기 때문에 어느정도 변경사항이 많은 경우 활용도가 높겠습니다. 버전은 float
형태로 정의하며 상대적으로 높은 값이 신규 배치가 됩니다. Context에서 지정하는 경우에는 어플리케이션 경로를 설정한 대로 샵 기호가 추가된 어플리케이션 이름을 지정하면 서비스 컨텍스트는 샵 기호와 버전이 제외된 기존 경로를 사용하게 됩니다.
<Context docBase="/app/sample##01" ... /></Context>
+
<Context docBase="/app/sample##02" ... /></Context>
+
배치된 어플리케이션은 톰캣 Manager에서도 확인할 수 있습니다.
버전이 높은 어플리케이션이 배치되면 기존 사용자는 이전 버전의 어플리케이션 서비스를 이용하고 새로 접속하는 사용자는 신규 어플리케이션의 서비스를 이용하게 됩니다. 이전 버전의 어플리케이션은 톰캣 Manager에서 Session을 확인하여 사용자가 없는것을 확인 후 제거할 수 있습니다.
`,5);function D(E,C){const s=o("ExternalLinkIcon");return l(),c("div",null,[i,r,d,m,a("ul",null,[a("li",null,[a("a",k,[n("http://ip"),e(s)]),n(":port 로 기본 톰캣 http 요청 (로컬에서 기본 설정으로 실행한 경우 "),a("a",g,[n("http://localhost:8080"),e(s)]),n(")")]),h,x,v,b]),y,_,q,w,A,f,a("p",null,[n("WebLogic Server 같은 타 WAS에서도 이와 유사한 '"),a("a",T,[n("production redeployment"),e(s)]),n("'기능이 있지만 톰캣이 좀더 쉬운 방법을 제공합니다.")]),O])}const I=p(u,[["render",D],["__file","05-deployment.html.vue"]]),R=JSON.parse(`{"path":"/05-Software/Tomcat/tomcat101/05-deployment.html","title":"5. Tomcat 애플리케이션 배포","lang":"ko-KR","frontmatter":{"description":"Tomcat","tag":["Tomcat","Java"],"head":[["meta",{"property":"og:url","content":"https://docmoa.github.io/05-Software/Tomcat/tomcat101/05-deployment.html"}],["meta",{"property":"og:site_name","content":"docmoa"}],["meta",{"property":"og:title","content":"5. Tomcat 애플리케이션 배포"}],["meta",{"property":"og:description","content":"Tomcat"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://raw.githubusercontent.com/Great-Stone/images/master/uPic/appVersion.jpg?token=ADUAZXOCLTBA3NROCYL7SF267EUQK"}],["meta",{"property":"og:locale","content":"ko-KR"}],["meta",{"property":"og:updated_time","content":"2023-09-19T11:31:31.000Z"}],["meta",{"name":"twitter:card","content":"summary_large_image"}],["meta",{"name":"twitter:image:alt","content":"5. Tomcat 애플리케이션 배포"}],["meta",{"property":"article:tag","content":"Tomcat"}],["meta",{"property":"article:tag","content":"Java"}],["meta",{"property":"article:modified_time","content":"2023-09-19T11:31:31.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"5. Tomcat 애플리케이션 배포\\",\\"image\\":[\\"https://raw.githubusercontent.com/Great-Stone/images/master/uPic/appVersion.jpg?token=ADUAZXOCLTBA3NROCYL7SF267EUQK\\"],\\"dateModified\\":\\"2023-09-19T11:31:31.000Z\\",\\"author\\":[]}"]]},"headers":[{"level":2,"title":"5.1 Web Application","slug":"_5-1-web-application","link":"#_5-1-web-application","children":[]},{"level":2,"title":"5.2 by Manager","slug":"_5-2-by-manager","link":"#_5-2-by-manager","children":[]},{"level":2,"title":"5.3 by webapps DIR","slug":"_5-3-by-webapps-dir","link":"#_5-3-by-webapps-dir","children":[]},{"level":2,"title":"5.4 by context.xml","slug":"_5-4-by-context-xml","link":"#_5-4-by-context-xml","children":[]},{"level":2,"title":"5.5 ROOT","slug":"_5-5-root","link":"#_5-5-root","children":[]},{"level":2,"title":"5.6 Auto Deployment & Hot Deployment","slug":"_5-6-auto-deployment-hot-deployment","link":"#_5-6-auto-deployment-hot-deployment","children":[{"level":3,"title":"5.6.1 webapps 'autoDeploy'","slug":"_5-6-1-webapps-autodeploy","link":"#_5-6-1-webapps-autodeploy","children":[]},{"level":3,"title":"5.6.2 jsp 'checkInterval'","slug":"_5-6-2-jsp-checkinterval","link":"#_5-6-2-jsp-checkinterval","children":[]},{"level":3,"title":"5.6.3 Class 'reloadable'","slug":"_5-6-3-class-reloadable","link":"#_5-6-3-class-reloadable","children":[]}]},{"level":2,"title":"5.7 Parallel Deployment","slug":"_5-7-parallel-deployment","link":"#_5-7-parallel-deployment","children":[]}],"git":{"createdTime":1640327880000,"updatedTime":1695123091000,"contributors":[{"name":"Great-Stone","email":"hahohh@gmail.com","commits":2},{"name":"Administrator","email":"admin@example.com","commits":1}]},"readingTime":{"minutes":2.35,"words":706},"filePathRelative":"05-Software/Tomcat/tomcat101/05-deployment.md","localizedDate":"2021년 12월 24일","excerpt":"\\nJenkins는 온라인에 연결된 plugin을 검색, 설치할 수 있는 플러그인 관리
기능을 갖고 있습니다. 좌측 메뉴에서 Jenkins 관리
를 클릭하면 플러그인 관리
링크를 통하여 해당 기능에 접근할 수 있습니다.
.hpi
확장자를 갖는 플러그인을 설치하거나 업데이트 사이트를 지정할 수 있습니다.각 플러그인 이름을 클릭하면 플러그인 정보를 확인할 수 있는 plugins.jenkins.io
사이트로 이동하여 정보를 보여줍니다. 사용방법은 우측에 wiki
링크를 클릭합니다. 대략적인 UI나 사용방법은 wiki.jenkins.io
에서 제공합니다.
소스의 var
디렉토리에는 Pipeline에서 사용하는 Shared Library들이 들어있습니다. groovy 스크립트로 되어있으며 Pipeline을 구성한 jenkinsfile
에서 이를 사용합니다.
vars/evenOdd.groovy
를 호출하고 값을 받아오는 형태를 갖고, evenOdd.groovy에서 사용하는 log.info
와 log.warning
은 vars/log.groovy
에 구현되어있습니다.
다음과 같이 Jenkins에 설정을 수행합니다.
Jenkins 관리
클릭 후 시스템 설정
을 선택합니다.Global Pipeline Libraries
의 추가 버튼을 클릭하여 새로운 구성을 추가합니다. Source Code Management
항목이 추가됩니다.GitHub
를 클릭하여 내용을 채웁니다. https://github.com/Great-Stone/evenOdd
인 경우 Great-Stone
이 Owner가 됩니다.Library
에 있는 Load implicitly
를 활성화 합니다.Shared Libraries가 준비가 되면 Pipeline
타입의 Item을 생성하고 (e.g. 05-02.UsingSharedLibraries) Pipeline 설정을 추가합니다.
저장 후 Build Now
를 클릭하여 빌드를 수행합니다. 빌드의 결과로는 2 단계로 수행되는데 1단계는 Declarative: Checkout SCM
으로 SCM으로부터 소스를 받아 준비하는 단계이고, 2단계는 jenkinsfile
을 수행하는 단계입니다. vars/evenOdd.goovy
스크립트에는 stage가 두개 있으나 해당 Pipeline 을 호출하는 값에 따라 하나의 stage만을 수행하도록 되어있어서 하나의 stage가 수행되었습니다.
// Jenkinsfile
+//@Library('evenOdd') _
+
+evenOdd(currentBuild.getNumber())
+
currentBuild.getNumber()
는 현재 생성된 Pipeline Item의 빌드 숫자에 따라 값을 evenOdd(빌드 숫자)
형태로 호출하게 됩니다.
Jenkins shared libraries를 사용하는 가장 좋은 예는 재사용성 있는 Groovy 함수를 타 작업자와 공유하는 것 입니다. 빌드의 상태는 다른 파이프 라인 단계로 계속할 것인지 결정하는 데 사용할 수도 있습니다.
경고
해당 설정은 모든 빌드에 영향을 주기 때문에 타 작업을 위해 추가된 Global Pipeline Libraries의 Library를 삭제하여 진행합니다.
Jenkins가 유용한 툴인 이유중 하나는 방대한 양의 플러그인 입니다. Jenkins의 기능을 확장시키고, 관리, 빌드 정책 등을 확장 시켜주고, 타 서비스와의 연계를 쉽게 가능하도록 합니다.
\\n\\n\\nTerraform은 stateful 애플리케이션입니다. 즉, state file 내부에서 빌드 한 모든 내용을 추적합니다.
앞서의 실습에서 반복된 Apply
작업 간에 Workspace 디렉토리에 나타난 terraform.tfstate
및 terraform.tfstate.backup
파일을 보셨을 것입니다.
상태 파일은 Terraform이 알고있는 모든 것에 대한 기록 소스입니다.
파일 구조
WORKSPACE
+├── files
+│ └── deploy_app.sh
+├── main.tf
+├── outputs.tf
+├── \`terraform.tfstate\`
+├── \`terraform.tfstate.backup\`
+├── terraform.tfvars
+└── variables.tf
+
State 파일 내부는 JSON 형식으로 구성되어있습니다.
{
+ "version": 4,
+ "terraform_version": "0.12.7",
+ "serial": 14,
+ "lineage": "452b4191-89f6-db17-a3b1-4470dcb00607",
+ "outputs": {
+ "catapp_url": {
+ "value": "http://go-hashicat-5c0265179ccda553.workshop.aws.hashidemos.io",
+ "type": "string"
+ },
+
때때로 인프라는 Terraform이 통제하는 범위 밖에서 변경 될 수 있습니다. (수동으로 UI에서 변경 등)
State 파일은 인프라의 마지막으로 갱신된 상태를 나타냅니다. 상태 파일이 빌드 한 파일과 여전히 일치하는지 확인하고 확인하려면 terraform refresh
명령을 사용할 수 있습니다.
이것은 인프라를 업데이트하지 않는 상태 파일 만 업데이트합니다.
terraform refresh
+
계획을 실행하거나 적용 할 때마다 Terraform은 세 가지 데이터 소스를 조정합니다.
Terraform은 *.tf
파일에있는 내용을 기반으로 기존 리소스를 추가, 삭제, 변경 또는 교체하기 위해 최선 을 다합니다. 다음은 Plan/Apply 중에 각 리소스에 발생할 수있는 네 가지 사항입니다.
+ create
+- destroy
+-/+ replace
+~ update in-place
+
경고
무엇인가 변경할때 -/+ replace
가 발생하는지 확인하세요. 이것은 기존 리소스를 삭제하고 다시 생성합니다.
각 시나리오에서 어떤 일이 발생합니까? 논의해 볼까요?
Configuration(.tf) | State | Reality | Operation |
---|---|---|---|
ncloud_server | ??? | ||
ncloud_server | ncloud_server | ??? | |
ncloud_server | ncloud_server | ncloud_server | ??? |
ncloud_server | ncloud_server | ??? | |
ncloud_server | ??? | ||
ncloud_server | ??? |
Configuration(.tf) | State | Reality | Operation |
---|---|---|---|
ncloud_server | create | ||
ncloud_server | ncloud_server | create | |
ncloud_server | ncloud_server | ncloud_server | - |
ncloud_server | ncloud_server | delete | |
ncloud_server | - | ||
ncloud_server | update state |
Terraform은 stateful 애플리케이션입니다. 즉, state file 내부에서 빌드 한 모든 내용을 추적합니다.
\\n앞서의 실습에서 반복된 Apply
작업 간에 Workspace 디렉토리에 나타난 terraform.tfstate
및 terraform.tfstate.backup
파일을 보셨을 것입니다.
상태 파일은 Terraform이 알고있는 모든 것에 대한 기록 소스입니다.
"}');export{u as comp,m as data}; diff --git a/assets/06-dbconnection.html-Lm8jzNRi.js b/assets/06-dbconnection.html-Lm8jzNRi.js new file mode 100644 index 0000000000..71fdb7f75f --- /dev/null +++ b/assets/06-dbconnection.html-Lm8jzNRi.js @@ -0,0 +1,115 @@ +import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{r as e,o as p,c as o,b as n,d as c,a as l,e as a}from"./app-Bzk8Nrll.js";const u={},i=n("h1",{id:"_6-tomcat-database-연동",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#_6-tomcat-database-연동"},[n("span",null,"6. Tomcat Database 연동")])],-1),r=n("ul",null,[n("li",null,"JDBC Connection Pool"),n("li",null,"DB 연동 예제"),n("li",null,"DB 연동 설정값"),n("li",null,"JNDI Lookup")],-1),k=n("iframe",{width:"560",height:"315",src:"https://www.youtube.com/embed/odsWlmZfzag",frameborder:"0",allow:"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture",allowfullscreen:""},null,-1),d=a(`JDBC Connection Pool은 Java에서 DB(Data Base)의 Session 자원을 미리 확보함으로 재생성하는 비용을 줄이기 위한 기술 입니다. Java에서 사용되는 기술이기 때문에 각 DB 벤더사들은 Java로 구현되는 서비스에서 자사의 DB를 사용할 수 있도록 별도의 라이브러리를 제공하며 이를 사용하여 DB와의 Connection을 생성하고 DB를 사용할 수 있게 됩니다.
JDBC는 여타 드라이버와는 다르게 미리 Connection을 확보하여 JVM상에 Object상태로 만들어두고 이를 요청하는 서비스에 빌려줍니다. 빌려준다는 표현을 사용한 이유는 반드시 반환되어야 하기 때문입니다. 앞서 미리 만든다는 표현은 만드는 개수가 제한되어 있다는 의미로 사용하였으며, 때문에 한정된 자원을 DB와의 연계 처리만을 하는 경우 잠시 사용하고 다시 반납하는 과정을 거칩니다.
다음은 jsp에서 단일 Oracle DB와의 Connection Pool을 생성하고 반납하는 샘플 코드입니다.(테스트 용도로 입니다.)
<%@ page import="java.sql.*" %>
+<%
+ StringBuffer sbError = new StringBuffer();
+ DatabaseMetaData dbMetaData = null;
+ Connection conn = null;
+%>
+<font size="-1"><p>
+<%
+ DriverManager.registerDriver (new oracle.jdbc.OracleDriver());
+ try {
+ conn = DriverManager.getConnection("jdbc:oracle:thin:@172.16.1.128:1521:TOSA1", "fmsvr", "fmsvr");
+ dbMetaData = conn.getMetaData();
+%>
+<p>
+Name of JDBC Driver: <%= dbMetaData.getDriverName() %><br>
+Version: <%= dbMetaData.getDriverVersion() %><br>
+Major: <%= dbMetaData.getDriverMajorVersion() %><br>
+Minor: <%= dbMetaData.getDriverMinorVersion() %><br>
+<p>
+Database Name: <%= dbMetaData.getDatabaseProductName() %><br>a
+Version: <%= dbMetaData.getDatabaseProductVersion() %><br>
+<%
+ } catch (SQLException e) {
+ sbError.append(e.toString());
+ } finally {
+ if (conn != null) {
+ try {
+ conn.close();
+ } catch (SQLException e) {
+ sbError.append(e.toString());
+ }
+ }
+ }
+ if (sbError.length() != 0) {
+ out.println(sbError.toString());
+ } else {
+%>
+<p>No error</font>
+<%
+ }
+%>
+
주어진 정보로 getConnection()을 수행하고 다시 close()를 수행하여 반납하는 과정이며, close()하지 않는 경우 해당 객체는 모두 사용하였음에도 불구하고 메모리상에 남아 차후 메모리 이슈를 발생시킬 수 있습니다.
톰캣에서는 이런 일련의 Connection을 생성하는 작업을 어플리케이션 대신 생성할 수 있으며 내부적으로 생성하는 개수나 연결이 끊어졌을 때의 재시도, 사용하지 않는 Connection의 강제 반환 등의 설정이 가능합니다.
다음은 Context
디스트립터 내에 설정하는 Resource
에서 정의한 DataSource 예제 입니다.
<Resource name="jdbc/test"
+ auth="Container"
+ type="javax.sql.DataSource"
+ username="oracle"
+ password="oracle"
+ driverClassName="oracle.jdbc.driver.OracleDriver"
+ url="jdbc:oracle:thin:@address:1521:SID"
+ removeAbandoned="true"
+ removeAbandonedTimeout="60"
+ logAbandoned="true"
+ maxActive="25"
+ maxIdle="10"
+ maxWait="-1"
+/>
+
톰캣에서 DB를 연동하기 위해서는 우선 사용할 DB의 벤더사에서 제공하는 JDBC driver를 ClassLoader에서 읽도록 해야 합니다. 우선 JDBC driver를 받고 두가지 방법으로 적용이 가능합니다.
#JDBC Driver Classpath
+CLASSPATH=/app/lib/jdbc.jar
+
대표적인 DB의 Context
디스크립터에 설정하는 방법은 다음과 같습니다.
<!-- MySQL - Connector/J -->
+<Resource name="jdbc/test"
+ auth="Container"
+ type="javax.sql.DataSource"
+ username="javauser"
+ password="javadude"
+ driverClassName="com.mysql.jdbc.Driver"
+ url="jdbc:mysql://ipaddress:3306/javatest"
+ maxActive="25"
+ maxIdle="10"
+ maxWait="-1"
+/>
+
<!-- Oracle - classes12.jar(jdk1.4.2), ojdbc#.jar(5+) -->
+<Resource name="jdbc/test"
+ auth="Container"
+ type="javax.sql.DataSource"
+ username="oracle"
+ password="oracle"
+ driverClassName="oracle.jdbc.driver.OracleDriver"
+ url="jdbc:oracle:thin:@ipaddress:1521:SID"
+ maxActive="25"
+ maxIdle="10"
+ maxWait="-1"
+ />
+
<!-- PostgreSQL - JDBC # -->
+<Resource name="jdbc/test"
+ auth="Container"
+ type="javax.sql.DataSource"
+ username="myuser"
+ password="mypasswd"
+ driverClassName="org.postgresql.Driver"
+ url="jdbc:postgresql://ipaddress:5432/mydb"
+ maxActive="25"
+ maxIdle="10"
+ maxWait="-1"
+/>
+
Resource
에 정의되는 항목은 기본적인 url이나 DB접근 계정정도만 있어도 가능하지만 간혹 튜닝이나 문제해결을 위해 추가적인 옵션이 요구되는 경우가 있습니다. 톰캣에서는 다음의 설정값을 제공합니다.
ATTRIBUTE | DESCRIPTOIN | DEFAULT |
---|---|---|
maxActive | 최대 Connection 값 | 100 |
maxIdle | Idle Connection 최대 허용치 | =maxActive |
minIdle | Idle Connection 최소 허용치 | =initialSize |
initialSize | Connection Pool의 최초 생성 개수 | 10 |
maxWait | Connection을 얻기위해 대기하는 최대 시간 | 30000(ms) |
removeAbandoned | 특정시간 동안 사용하지 않는 Connection 반환 | false |
removeAbandonedTimeout | removeAbandoned가 동작하는데 소요되는 시간 | 60(s) |
logAbandoned | Connection이 remove될 때 log에 기록 | false |
testOnBorrow | getConnection()이 수행될 때 유효성 테스트 | false |
validationQuery | 테스트를 위한 쿼리 | null |
validationQuery
로는 다음과 같이 적용 가능합니다.
톰캣에서 생성한 JDBC Connection Pool을 DataSource로서 사용하기 위해서는 JNDI를 Lookup하는 방법을 사용합니다. JNDI를 사용하면 이를 지원하는 다른 프레임워크나 API에서도 톰캣의 자원을 사용할 수 있습니다. mybatis/ibatis의 경우도 JNDI 설정을 할 수 있습니다.
Context
디스크립터로 정의한 DataSource는 어플리케이션의 web.xml
에서 정의하고 소스에서는 lookup
을 이용하여 사용합니다. 일련의 설정방법의 예는 다음과 같습니다. Context의 정의의 위치에 따라 전체 어플리케이션에 적용될 수도 있고 host단위, 혹은 단일 어플리케이션 내에서만 자원을 생성하게 됩니다.
<!-- context.xml -->
+<Resource name="jdbc/test"
+ auth="Container"
+ type="javax.sql.DataSource"
+ username="oracle"
+ password="oracle"
+ driverClassName="oracle.jdbc.driver.OracleDriver"
+ url="jdbc:oracle:thin:@ipaddress:1521:SID"
+ />
+
<!-- web.xml -->
+<web-app>
+ ...
+ <resource-ref>
+ <res-ref-name>jdbc/test</res-ref-name>
+ <res-type>javax.sql.DataSource</res-type>
+ <res-auth>Container</res-auth>
+ </resource-ref>
+ ...
+</web-app>
+
//Source Code
+ds = ctx.lookup("java:comp/env/jdbc/test");
+
jenkins를 검색하여 Yet Another Jenkins Notifier
를 확인합니다. Chrome에 추가
버튼으로 확장 프로그램을 설치합니다.
설치가 완료되면 브라우저 우측 상단에 Jenkins 아이콘이 나타납니다. 클릭합니다.
각 Item(Job)의 url을 입력하여 +
버튼을 클릭합니다.
등록된 Item을 확인하고 해당 빌드를 Jenkins 콘솔에서 실행해봅니다. 결과에 대한 알림이 발생하는 것을 확인 할 수 있습니다.
Jenkins에서 빌드가 수행된 결과를 SCM에 반영하는 기능도 플러그인을 통해 가능합니다. SCM에서 해당 Jenkins에 접근이 가능해야 하므로 Jenkins는 SCM에서 접근가능한 네트워크 상태여야 합니다.
Jenkins에 새로운 플러그인을 추가하고 설정합니다.
Jenkins 관리
로 이동하여 플러그인 관리
를 클릭합니다.설치 가능
탭을 클릭하고 상단의 검색에 embed
를 입력하면 Embeddable Build Status
플러그인이 나타납니다. 선택하여 설치를 진행합니다.Jenkins 관리
로 이동하여 Configure Global Security
을 클릭합니다.Authorization
항목에서 Matrix-based security
를 체크합니다.Authenticated Users
의 경우 필요한 각 항목에 대해 체크박스를 활성화 합니다.Anonymous Users
에 대해서 Job/ViewStatus
항목을 활성화 합니다.이제 기존의 외부 SCM이 연결된 Item을 선택합니다. 여기서는 05-02.UsingSharedLibraries
에 설정합니다. 해당 Item을 선택하면 좌측에 Embeddable Build Status
항목이 새로 생긴것을 확인 할 수 있습니다.
해당 항목을 클릭하고 Markdown
의 unprotected
의 항목을 복사합니다.
[![Build Status](http://myjenkins.com/buildStatus/icon?job=05-02.UsingSharedLibraries)](http://myjenkins.com/job/05-02.UsingSharedLibraries/)
+
# evenOdd
+[![Build Status](http://myjenkins.com/buildStatus/icon?job=libraries)](http://myjenkins.com/job/libraries/)
+
+A Jenkins even/odd playbook from the Jenkins.io documentation
+
+Add this as a shared library called evenOdd in your jenkins
+instance, and then instantiate the pipeline in your project Jenkinsfile
+
+This will also use an example of global variabls from the log.groovy
+definitions
+
+
이같이 반영하면 각 빌드에 대한 결과를 SCM에 동적으로 상태를 반영 할 수 있습니다.
이같은 알림 설정은 코드의 빌드가 얼마나 잘 수행되는지 이해하고 추적할 수 있도록 도와줍니다.
',4);function w(N,C){const o=a("ExternalLinkIcon"),s=a("RouteLink");return m(),u("div",null,[b,g,_,e("p",null,[i("Jenkins에서는 플러그인이나 외부 툴에 의해 빌드에 대한 결과를 받아 볼 수 있습니다. 대표적으로는 Jenkins의 슬랙 플러그인을 사용하여 슬랙으로 빌드에 결과를 받아보거나, "),e("a",k,[i("catlight.io"),t(o)]),i(" 에서 데스크탑용 어플리케이션에 연동하는 방법도 있습니다.")]),v,y,e("ol",null,[e("li",null,[e("p",null,[t(s,{to:"/05-Software/Jenkins/pipeline101/chrome:/apps/"},{default:h(()=>[i("chrome://apps/")]),_:1}),i("에 접속하여 앱스토어를 클릭합니다.")])]),S]),J,e("p",null,[i("복사한 형식을 GitHub의 evenOdd repository의 "),e("a",j,[i("README.md"),t(o)]),i(" 파일 상단에 위치 시킵니다.")]),x])}const E=p(f,[["render",w],["__file","06-notifications.html.vue"]]),L=JSON.parse('{"path":"/05-Software/Jenkins/pipeline101/06-notifications.html","title":"6. Notifications","lang":"ko-KR","frontmatter":{"description":"jenkins 101","tag":["cicd","jenkins"],"head":[["meta",{"property":"og:url","content":"https://docmoa.github.io/05-Software/Jenkins/pipeline101/06-notifications.html"}],["meta",{"property":"og:site_name","content":"docmoa"}],["meta",{"property":"og:title","content":"6. Notifications"}],["meta",{"property":"og:description","content":"jenkins 101"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"http://myjenkins.com/buildStatus/icon?job=05-02.UsingSharedLibraries"}],["meta",{"property":"og:locale","content":"ko-KR"}],["meta",{"property":"og:updated_time","content":"2023-09-18T13:12:54.000Z"}],["meta",{"name":"twitter:card","content":"summary_large_image"}],["meta",{"name":"twitter:image:alt","content":"6. Notifications"}],["meta",{"property":"article:tag","content":"cicd"}],["meta",{"property":"article:tag","content":"jenkins"}],["meta",{"property":"article:modified_time","content":"2023-09-18T13:12:54.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"6. Notifications\\",\\"image\\":[\\"http://myjenkins.com/buildStatus/icon?job=05-02.UsingSharedLibraries\\",\\"http://myjenkins.com/buildStatus/icon?job=libraries\\"],\\"dateModified\\":\\"2023-09-18T13:12:54.000Z\\",\\"author\\":[]}"]]},"headers":[{"level":2,"title":"6.1 Notifications of build state","slug":"_6-1-notifications-of-build-state","link":"#_6-1-notifications-of-build-state","children":[]},{"level":2,"title":"6.2 Build state badges for SCM","slug":"_6-2-build-state-badges-for-scm","link":"#_6-2-build-state-badges-for-scm","children":[]}],"git":{"createdTime":1640327880000,"updatedTime":1695042774000,"contributors":[{"name":"Administrator","email":"admin@example.com","commits":1},{"name":"Great-Stone","email":"hahohh@gmail.com","commits":1}]},"readingTime":{"minutes":0.62,"words":187},"filePathRelative":"05-Software/Jenkins/pipeline101/06-notifications.md","localizedDate":"2021년 12월 24일","excerpt":"\\nJenkins빌드의 결과를 받아볼 수 있는 몇가지 방안에 대해 알아봅니다.
\\nJenkins에서는 플러그인이나 외부 툴에 의해 빌드에 대한 결과를 받아 볼 수 있습니다. 대표적으로는 Jenkins의 슬랙 플러그인을 사용하여 슬랙으로 빌드에 결과를 받아보거나, catlight.io 에서 데스크탑용 어플리케이션에 연동하는 방법도 있습니다.
"}');export{E as comp,L as data}; diff --git a/assets/06-terraform-cloud.html-BOv2Y4DV.js b/assets/06-terraform-cloud.html-BOv2Y4DV.js new file mode 100644 index 0000000000..6cdcfb80b0 --- /dev/null +++ b/assets/06-terraform-cloud.html-BOv2Y4DV.js @@ -0,0 +1 @@ +import{_ as e}from"./plugin-vue_export-helper-DlAUqK2U.js";import{r as o,o as t,c as a,b as l,a as i,w as n,e as m,d as c}from"./app-Bzk8Nrll.js";const p="/assets/automate-the-provisioning-lifecycle-h9sTQYF2.png",d="/assets/tfc_tfe_logo-BObqu3et.png",s={},f=m('Terraform Cloud는 Terraform을 사용하여 코드로 인프라를 작성하고 구축하기위한 최고의 워크 플로를 제공하는 무료 로 시작하는 SaaS 애플리케이션입니다.
Terraform Cloud는 Remote State 관리, API 기반 실행, 정책 관리 등과 같은 기능을 제공하는 호스팅 된 애플리케이션입니다. 많은 사용자가 클라우드 기반 SaaS 솔루션을 선호하는 이유 중 한가지는 인프라를 유지하여 실행하는 것이 부담될 때 입니다.
Terraform Enterprise는 동일한 애플리케이션이지만 클라우드 환경이나 데이터 센터에서 실행됩니다. 일부 사용자는 Terraform Enterprise 애플리케이션에 대한 더 많은 제어가 필요하거나 회사 방화벽 뒤의 제한된 네트워크에서 실행하려고합니다.
이 두 제품의 기능 목록은 거의 동일합니다. 다음 실습에서는 Terraform Cloud 계정을 사용할 것입니다.
기본적으로 Terraform은 랩톱 또는 워크스테이션의 Workspace 디렉토리에 State 파일을 저장합니다. 이것은 개발 및 테스트에는 괜찮지만 프로덕션 환경에서는 상태 파일을 안전하게 보호하고 저장해야합니다.
Terraform에는 상태 파일을 원격으로 저장하고 보호하는 옵션이 있습니다. Terraform Cloud 계정은 이제 오픈 소스 사용자에게도 무제한 상태 파일 스토리지를 제공합니다.
모든 상태 파일은 암호화되어 (HashiCorp Vault 사용) Terraform Cloud 계정에 안전하게 저장됩니다. 상태 파일을 다시 잃어 버리거나 삭제하는 것에 대해 걱정할 필요가 없습니다.
Local Run - Terraform 명령은 랩톱 또는 워크 스테이션에서 실행되며 모든 변수는 로컬로 구성됩니다. 테라 폼 상태 만 원격으로 저장됩니다.
Remote Run - Terraform 명령은 Terraform Cloud 컨테이너 환경에서 실행됩니다. 모든 변수는 원격 작업 공간에 저장됩니다. 코드는 Version Control System 저장소에 저장할 수 있습니다. 프리 티어 사용자의 경우 동시 실행이 1 회로 제한됩니다.
Agent Run - Terraform Cloud에서 내부네트워크에 있는 환경(VM, ldap 등)을 프로비저닝 하고자 할 때 내부에 실행을 위한 에이전트를 구성할 수 있습니다. Terraform Enterprise에서는 프로비저닝을 위한 프로세스를 여러 서버로 분산시킵니다.
실습을 위해 다음장으로 이동하세요.
',18);function u(h,T){const r=o("RouteLink");return t(),a("div",null,[f,l("p",null,[i(r,{to:"/03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/06-z-lab_terraform_cloud.html"},{default:n(()=>[c("💻 Lab - Terraform Cloud 연결")]),_:1})])])}const C=e(s,[["render",u],["__file","06-terraform-cloud.html.vue"]]),k=JSON.parse('{"path":"/03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/06-terraform-cloud.html","title":"06. Terraform Cloud","lang":"ko-KR","frontmatter":{"description":"Naver Cloud Platform에서의 Terraform 실습","tag":["ncloud","ncp","terraform","workshop"],"head":[["meta",{"property":"og:url","content":"https://docmoa.github.io/03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/06-terraform-cloud.html"}],["meta",{"property":"og:site_name","content":"docmoa"}],["meta",{"property":"og:title","content":"06. Terraform Cloud"}],["meta",{"property":"og:description","content":"Naver Cloud Platform에서의 Terraform 실습"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"ko-KR"}],["meta",{"property":"og:updated_time","content":"2023-09-18T13:12:54.000Z"}],["meta",{"property":"article:tag","content":"ncloud"}],["meta",{"property":"article:tag","content":"ncp"}],["meta",{"property":"article:tag","content":"terraform"}],["meta",{"property":"article:tag","content":"workshop"}],["meta",{"property":"article:modified_time","content":"2023-09-18T13:12:54.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"06. Terraform Cloud\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2023-09-18T13:12:54.000Z\\",\\"author\\":[]}"]]},"headers":[{"level":2,"title":"Terraform Cloud","slug":"terraform-cloud","link":"#terraform-cloud","children":[]},{"level":2,"title":"","slug":"","link":"#","children":[]},{"level":2,"title":"Terraform Remote State","slug":"terraform-remote-state","link":"#terraform-remote-state","children":[]},{"level":2,"title":"Terraform Cloud Execution Modes","slug":"terraform-cloud-execution-modes","link":"#terraform-cloud-execution-modes","children":[]}],"git":{"createdTime":1695042774000,"updatedTime":1695042774000,"contributors":[{"name":"Great-Stone","email":"hahohh@gmail.com","commits":1}]},"readingTime":{"minutes":0.38,"words":114},"filePathRelative":"03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/06-terraform-cloud.md","localizedDate":"2023년 9월 18일","excerpt":"\\nTerraform Cloud는 Terraform을 사용하여 코드로 인프라를 작성하고 구축하기위한 최고의 워크 플로를 제공하는 무료 로 시작하는 SaaS 애플리케이션입니다.
\\n\\nOpen Folder...
를 클릭합니다.lab02
을 열어줍니다.@slidestart blood
@slideend
Terraform Cloud는 다른 SaaS 서비스와 같이 개인을 위한 무료 플랜이 준비되어있습니다.
아직 계정이 없는 경우 계성을 생성하고 다음 실습을 진행합니다.
',13),C={href:"https://app.terraform.io/signup/account",target:"_blank",rel:"noopener noreferrer"},x=e("li",null,[a("필요한 정보를 입력하고 확인하여 신규 계정을 생성합니다. "),e("button",{style:{"border-color":"#3322de","background-color":"#5c4ee5",color:"#fff","font-size":"1rem"}},"Cretea account")],-1),q=e("li",null,"가입한 이메일로 계정 생성 확인 메시지가 도착합니다. 링크를 확인하면 Terraform Cloud를 사용할 준비가 끝났습니다.",-1),w=n('Terraform Cloud에 로그인하면 YOURNAME-training
이라는 새 조직을 만듭니다. YOURNAME
을 자신의 이름이나 다른 텍스트로 바꾸십시오.
다음으로 Workspace를 생성하라는 메시지가 표시됩니다. CLI 기반 워크플로
패널을 클릭하여 VCS 통합 단계를 건너뛸 수 있습니다.
작업 공간의 이름을 hashicat-ncp
로 지정 하고 Create workspace
를 클릭하여 새로운 Workspace를 생성합니다.
터미널에서 terraform version
을 실행하여 버전을 확인합니다.
Terraform Cloud 상에 생성한 hashicat-ncp
의 Settings > General
로 이동하여 Terraform Version
을 동일한 버전으로 구성합니다. 그리고 Execution Mode를 Local
로 설정합니다.
Settings
페이지 하단에 버튼을 클릭하여 저장합니다.@slidestart blood
@slideend
이번 실습에서는 Terraform Cloud를 Remote State Backend로 구성하여 기존 State 파일을 Terraform Cloud 환경으로 마이그레이션 합니다.
Workspace 디렉토리에 (main.tf
와 같은 위치) 아래와 같은 내용으로 remote_backend.tf
파일을 생성합니다.
# remote_backend.tf
+terraform {
+ backend "remote" {
+ hostname = "app.terraform.io"
+ organization = "YOURORGANIZATION"
+ workspaces {
+ name = "hashicat-ncp"
+ }
+ }
+}
+
YOURORGANIZATION
을 생성한 Organization 이름으로 수정합니다.
이후 터미널에서 terraform login
을 입력합니다. 로컬 환경에 Terraform Cloud와 API 인증을 위한 Token을 생성하는 과정입니다. yes
를 입력하면 Terraform Cloud의 토큰 생성화면이 열립니다.
생성된 Token을 복사하여 앞서 터미널에 새로운 입력란인 Enter a value:
에 붙여넣고 ⏎(엔터)를 입력합니다. (입력된 값은 보이지 않습니다.)
...
+Generate a token using your browser, and copy-paste it into this prompt.
+
+Terraform will store the token in plain text in the following file
+for use by subsequent commands:
+ /Users/yourname/.terraform.d/credentials.tfrc.json
+
+Token for app.terraform.io:
+ Enter a value: ******************************************
+
해당 토큰은 터미널에 표기된 credentials.tfrc.json
파일에 저장됩니다.
터미널에서 terraform init
을 실행합니다.
State를 Terraform Cloud로 마이그레이션하라는 메시지가 표시되면 "yes"를 입력합니다.
backend가 remote로 구성됨이 성공함을 확인합니다.
`,6),E=e("div",{class:"language-bash","data-ext":"sh","data-title":"sh"},[e("pre",{bash:"",class:"language-bash"},[e("code",null,[a(`$ terraform init +`),e("span",{class:"token punctuation"},".."),a(`. +Initializing the backend`),e("span",{class:"token punctuation"},".."),a(`. + +Successfully configured the backend `),e("span",{class:"token string"},'"remote"'),e("span",{class:"token operator"},"!"),a(` Terraform will automatically +use this backend unless the backend configuration changes. +`),e("span",{class:"token punctuation"},".."),a(`. +`)])]),e("div",{class:"highlight-lines"},[e("div",{class:"highlight-line"}," "),e("br"),e("br"),e("br"),e("br"),e("br"),e("br")])],-1),B=n('이제 상태가 Terraform Cloud에 안전하게 저장됩니다. TFC UI에서 작업 영역의 "State" 탭에서 이를 확인할 수 있습니다.
변수들을 변경하면서 terraform apply -auto-approve
를 실행하고, 상태 파일이 리소스가 변경될 때마다 변경되는 것을 지켜보십시오. Terraform Cloud UI를 사용하여 이전 상태 파일을 탐색할 수 있습니다.
@slidestart blood
@slideend
다음 명령을 실행하여 인프라를 삭제하세요.
terraform destroy
+
인프라를 삭제한다는 메시지가 표시되면 "yes"를 입력해야 합니다. 중요한 리소스가 실수로 삭제되는 것을 방지하기 위한 안전 기능입니다.
확인 버튼을 클릭하기 전에 리소스 삭제 작업이 완전히 끝날 때까지 기다리십시오.
Open Folder...
를 클릭합니다.lab02
을 열어줍니다.@slidestart blood
\\n톰캣에 정의된 바로는 Host
로 정의되나 일반적인 기능으로 표현한다면 가상 호스트(Virtual Host)와 같은 기능 입니다. 특정 host 명, 즉 http url로 서비스를 분기하는 역할을 합니다. server.xml
기본으로 설정되어있는 localhost
인 호스트의 내용은 다음과 같습니다.
<Engine name="Catalina" defaultHost="localhost">
+ <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
+ ...
+ </Host>
+</Engine>
+
설정된 내용을 분석해본다면 Catalina
라는 톰캣의 엔진에서 처리하는 기본 호스트는 localhost
이고, localhost
는 webapps
디렉토리를 기본 배치 디렉토리고 갖는다는 내용입니다. 기본 호스트로 지정된 호스트는 이외에 설정된 호스트 조건에 맞지 않은 모든 요청을 처리하게 됩니다. 이렇게 생성된 localhost
는 $CATALINA_HOME/conf/[ENGINENAME]/[HOSTNAME]
과 같은 경로에 호스트만의 설정 값을 갖게 됩니다.
별도의 호스트 추가는 'Engine' 디스크립터 하위에 'Host' 디스크립터로 정의합니다. 'myhost'라는 호스트는 다음과 같이 추가할 수 있습니다.
<Engine name="Catalina" defaultHost="localhost">
+ <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
+ ...
+ </Host>
+ <Host name="myhost" appBase="webapps_myhost" unpackWARs="true" autoDeploy="true">
+ ...
+ </Host>
+</Engine>
+
새롭게 추가되는 호스트는 기본 배치경로를 다르게 설정합니다. 때문에 동일한 컨텍스트의 요청이라도 어떤 호스트가 처리하는가에 따라 다른 어플리케이션의 서비스를 이용하게 됩니다. 설정 후 톰캣 프로세스를 재기동하면 호스트 설정 디렉토리가 생성됨을 확인 할 수 있습니다.
호스트의 기능은 주로 웹서버에서 많이 사용되는 기능입니다. 특정 url로 호출되는 요청을 각 요청 전달 목적지에 맞게 분배하는 역할을 수행하지요. 호스트는 톰캣에도 구현되어 있으며 이를 통해 하나의 톰캣 내에서 같은 컨텍스트를 갖는 요청의 처리가 가능합니다.
팁
톰캣의 프로세스를 서비스 마다 생성하지 못하는 상황(e.g. 자원의 한계와 같은) 또는 서비스에서 소모하는 자원이 크지 않아 추가적인 프로세스를 기동하지 않아도 되는 상황 등 호스트의 기능을 활용 할수 있습니다.
호스트를 관리하기 위해 톰캣에서는 'host manager'를 제공합니다. 앞서 배치에서 보았던 'Manage APP'와 같이 호스트를 쉽게 구성할 수 있도록 UI환경을 제공합니다. 이를 통해 설정파일에 직접 수정하기보다 제공되는 UI로 정확하고 쉽게 구성하고 관리할 수 있도록 합니다.
`,14),d={href:"http://ip",target:"_blank",rel:"noopener noreferrer"},g={href:"http://localhost:8080",target:"_blank",rel:"noopener noreferrer"},h=a("li",null,[t("ROOT 어플리케이션의 호출 확인 후 좌/우측의 "),a("code",null,"Host Manager"),t(" 클릭")],-1),q=a("li",null,"톰캣 유저의 설정이 되어있지 않으므로 로그인 창에서 '취소'버튼 클릭",-1),_=a("li",null,[a("code",null,"$CATALINA_HOME/conf/tomcat-user.xml"),t(" 에 설정하는 방법을 에러페이지에서 확인")],-1),f=a("li",null,[t("tomcat-user.xml에 다음과 같이 설정 추가 (e.g. "),a("code",null,"user/passwd"),t("를 "),a("code",null,"admin/admin"),t("으로 설정)")],-1),v=e(`<tomcat-users>
+ <role rolename="manager-gui"/>
+ <role rolename="admin-gui"/>
+ <user username="admin" password="admin" roles="manager-gui,admin-gui"/>
+</tomcat-users>\`\`\`
+
user/passwd
입력 후 로그인host manager
에서는 기본 호스트 외에 추가적인 호스트에 대한 추가를 할 수 있도록 Add Virtual Host
를 사용할 수 있습니다. Host
디스크립터에서 정의되는 내용을 각 항목에 맞게 입력 할 수 있고 이렇게 추가된 호스트는 Host name
테이블의 commands
에서 개별적으로 시작과 정지가 가능합니다. 호스트가 정지되어 비활성화 된 상태에서는 해당 호스트의 요청 url에 맞게 들어오더라도 기본 호스트가 처리하게 됩니다. host manager
는 웹페이지를 통한 호스트의 추가/삭제/컨트롤이 가능하므로 외부, 또는 관리자가 아닌 사용자가 접근하지 못하도록 해야 합니다.
톰캣에 정의된 바로는 Host
로 정의되나 일반적인 기능으로 표현한다면 가상 호스트(Virtual Host)와 같은 기능 입니다. 특정 host 명, 즉 http url로 서비스를 분기하는 역할을 합니다. server.xml
기본으로 설정되어있는 localhost
인 호스트의 내용은 다음과 같습니다.
테스트 Pipeline 구성시 테스트 과정을 지정할 수 있습니다. Testing을 위한 Pipeline
타입의 Item을 추가로 생성합니다. (e.g. 07-01.CodeCoverageTestsAndReports)
설정은 다음과 같이 수행합니다.
Pipeline
스크립트에 다음과 같이 입력 합니다. 테스트와 빌드, 검증 후 결과를 보관하는 단계까지 이루어 집니다.
pipeline {
+ agent any
+ stages {
+ stage('Build') {
+ steps {
+ sh '''
+ echo This > app.sh
+ echo That >> app.sh
+ '''
+ }
+ }
+ stage('Test') {
+ steps {
+ sh '''
+ grep This app.sh >> \${BUILD_ID}.cov
+ grep That app.sh >> \${BUILD_ID}.cov
+ '''
+ }
+ }
+ stage('Coverage'){
+ steps {
+ sh '''
+ app_lines=\`cat app.sh | wc -l\`
+ cov_lines=\`cat \${BUILD_ID}.cov | wc -l\`
+ echo The app has \`expr $app_lines - $cov_lines\` lines uncovered > \${BUILD_ID}.rpt
+ cat \${BUILD_ID}.rpt
+ '''
+ archiveArtifacts "\${env.BUILD_ID}.rpt"
+ }
+ }
+ }
+}
+
빌드가 완료되면 해당 Job화면을 리로드 합니다. Pipeline에 archiveArtifacts
가 추가되었으므로 해당 Job에서 이를 관리합니다.
해당 아카이브에는 코드 검증 후의 결과가 저장 됩니다.
테스트 결과에 따라 빌드를 중지시키는 Pipeline 스크립트를 확인합니다. Testing을 위한 Pipeline
타입의 Item을 추가로 생성합니다. (e.g. 07-02.UsingTestResultsToStopTheBuild)
설정은 다음과 같이 수행합니다.
Pipeline
스크립트에 다음과 같이 입력 합니다. 테스트와 빌드, 검증 후 결과를 보관하는 단계까지 이루어 집니다.
pipeline {
+ agent any
+ stages {
+ stage('Build') {
+ steps {
+ sh '''
+ echo This > app.sh
+ echo That >> app.sh
+ echo The Other >> app.sh
+ '''
+ }
+ }
+ stage('Test') {
+ steps {
+ sh '''
+ for n in This That Those
+ do if grep $n app.sh >> \${BUILD_ID}.cov
+ then exit 1
+ fi
+ done
+ '''
+ }
+ }
+ stage('Coverage'){
+ steps {
+ sh '''
+ app_lines=\`cat app.sh | wc -l\`
+ cov_lines=\`cat \${BUILD_ID}.cov | wc -l\`
+ echo The app has \`expr $app_lines - $cov_lines\` lines uncovered > \${BUILD_ID}.rpt
+ cat \${BUILD_ID}.rpt
+ '''
+ archiveArtifacts "\${env.BUILD_ID}.rpt"
+ }
+ }
+ }
+}
+
저장을 하고 빌드를 수행하면, Pipeline 스크립트 상 Test
Stage에서 조건 만족 시 exit 1
를 수행하므로 빌드는 중간에 멈추게 됩니다.
테스트 Pipeline 구성시 테스트 과정을 지정할 수 있습니다. Testing을 위한 Pipeline
타입의 Item을 추가로 생성합니다. (e.g. 07-01.CodeCoverageTestsAndReports)
설정은 다음과 같이 수행합니다.
\\nPipeline
스크립트에 다음과 같이 입력 합니다. 테스트와 빌드, 검증 후 결과를 보관하는 단계까지 이루어 집니다.
pipeline {\\n agent any\\n stages {\\n stage('Build') {\\n steps {\\n sh '''\\n echo This > app.sh\\n echo That >> app.sh\\n '''\\n }\\n }\\n stage('Test') {\\n steps {\\n sh '''\\n grep This app.sh >> \${BUILD_ID}.cov\\n grep That app.sh >> \${BUILD_ID}.cov\\n '''\\n }\\n }\\n stage('Coverage'){\\n steps {\\n sh '''\\n app_lines=\`cat app.sh | wc -l\`\\n cov_lines=\`cat \${BUILD_ID}.cov | wc -l\`\\n echo The app has \`expr $app_lines - $cov_lines\` lines uncovered > \${BUILD_ID}.rpt\\n cat \${BUILD_ID}.rpt\\n '''\\n archiveArtifacts \\"\${env.BUILD_ID}.rpt\\"\\n }\\n }\\n }\\n}\\n
빌드가 완료되면 해당 Job화면을 리로드 합니다. Pipeline에 archiveArtifacts
가 추가되었으므로 해당 Job에서 이를 관리합니다.
\\n
해당 아카이브에는 코드 검증 후의 결과가 저장 됩니다.
\\nJenkins는 외부 서비스와의 연동이나 정보 조회를 위한 API를 제공합니다.
Jenkins REST API 테스트를 위해서는 Jenkins에 인증 가능한 Token을 취득하고 curl이나 Postman 같은 도구를 사용하여 확인 가능 합니다. 우선 Token을 얻는 방법은 다음과 같습니다.
Jenkins에 로그인 합니다.
우측 상단의 로그인 아이디에 마우스를 호버하면 드롭박스 버튼이 나타납니다. 설정
을 클릭합니다.
API Token
에서 Current token
을 확인합니다. 등록된 Token이 없는 경우 다음과 같이 신규 Token을 발급 받습니다.
ADD NEW TOKEN
을 클릭합니다.
이름을 기입하는 칸에 로그인 한 아이디를 등록합니다. (e.g. admin)
GENERATE
를 클릭하여 Token을 생성합니다.
이름과 Token을 사용하여 다음과 같이 curl로 접속하면 Jenkins-Crumb
프롬프트가 나타납니다.
$ curl --user "admin:TOKEN" 'http://myjenkins.com/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,":",//crumb)'
+
+Jenkins-Crumb:89e1fd9c402824c89465f6b97f49b605
+
Crumb
를 확인했으면 다시 헤더 값에 Jenkins-Crumb:
를 추가하여 02-04.MultiStep
Job을 빌드하기 위해 다음과 같이 요청합니다.
$ curl -X POST http://myjenkins.com/job/02-04.MultiStep/build --user gyulee:11479bdec9cada082d189938a3946348be --data-urlencode json='' -H "Jenkins-Crumb:89e1fd9c402824c89465f6b97f49b605"
+
API로 호출된 빌드가 수행되어 빌드 번호가 증가하는 것을 확인합니다.
빌드에 대한 결과를 REST API를 통해 요청하는 방법을 알아봅니다. 앞서 진행시의 Token값이 필요합니다. Json 형태로 출력되기 때문에 정렬을 위해 python이 설치 되어있다면 mjson.tool
을 사용하여 보기 좋은 형태로 출력 가능합니다.
# Python이 설치되어있지 않은 경우
+$ yum -y install python2
+
+# Jenkins에 REST API로 마지막 빌드 상태 요청
+$ curl -s --user gyulee:11479bdec9cada082d189938a3946348be http://myjenkins.com/job/02-04.MultiStep/lastBuild/api/json | python2 -mjson.tool
+
+{
+ "_class": "org.jenkinsci.plugins.workflow.job.WorkflowRun",
+ "actions": [
+ {
+ "_class": "hudson.model.CauseAction",
+ "causes": [
+ {
+ "_class": "hudson.model.Cause$UserIdCause",
+ "shortDescription": "Started by user GyuSeok.Lee",
+ "userId": "gyulee",
+ "userName": "GyuSeok.Lee"
+ }
+ ]
+ },
+ {},
+ {
+ "_class": "hudson.plugins.git.util.BuildData",
+ "buildsByBranchName": {
+ "master": {
+ "_class": "hudson.plugins.git.util.Build",
+ "buildNumber": 5,
+ "buildResult": null,
+...
+
Jenkins는 외부 서비스와의 연동이나 정보 조회를 위한 API를 제공합니다.
\\nJenkins REST API 테스트를 위해서는 Jenkins에 인증 가능한 Token을 취득하고 curl이나 Postman 같은 도구를 사용하여 확인 가능 합니다. 우선 Token을 얻는 방법은 다음과 같습니다.
\\nJenkins에 로그인 합니다.
\\n우측 상단의 로그인 아이디에 마우스를 호버하면 드롭박스 버튼이 나타납니다. 설정
을 클릭합니다.
API Token
에서 Current token
을 확인합니다. 등록된 Token이 없는 경우 다음과 같이 신규 Token을 발급 받습니다.
ADD NEW TOKEN
을 클릭합니다.
이름을 기입하는 칸에 로그인 한 아이디를 등록합니다. (e.g. admin)
\\nGENERATE
를 클릭하여 Token을 생성합니다.
이름과 Token을 사용하여 다음과 같이 curl로 접속하면 Jenkins-Crumb
프롬프트가 나타납니다.
$ curl --user \\"admin:TOKEN\\" 'http://myjenkins.com/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,\\":\\",//crumb)'\\n\\nJenkins-Crumb:89e1fd9c402824c89465f6b97f49b605\\n
Crumb
를 확인했으면 다시 헤더 값에 Jenkins-Crumb:
를 추가하여 02-04.MultiStep
Job을 빌드하기 위해 다음과 같이 요청합니다.
$ curl -X POST http://myjenkins.com/job/02-04.MultiStep/build --user gyulee:11479bdec9cada082d189938a3946348be --data-urlencode json='' -H \\"Jenkins-Crumb:89e1fd9c402824c89465f6b97f49b605\\"\\n
톰캣 단일로 서비스하는 경우도 있지만 일반적으로 웹서버와 연동하여 사용하는 경우가 보다 더 많습니다. 그 이유를 다음과 같이 정리합니다.
톰캣에서 처리하는 서비스 요청이 증가하면 단일 프로세스로 처리가 부족한 상황이 발생합니다. 처리에 필요한 힙 메모리를 추가해야 한다면 현재 적용된 메모리 설정보다 더 많은 값을 설정하고 CPU 자원이 부족하다면 장비의 교체도 고려해 봐야겠습니다. 이런 접근은 가용한 메모리가 없거나 더 나은 장비를 추가 구입/구성 해야하는 점이 있습니다. 따라서 단일 프로세스의 한계를 유연하게 대처하기 위해 복수의 프로세스에서 동일한 서비스를 구성하는 방안을 고려할 수 있습니다. 그리고 복수의 프로세스에 요청을 전달하기 위해 LB(LoadBalancer)가 필요합니다.
LB 기능을 수행하는 대표적인 두가지는 네트워크 장비(스위치 장비: L2, L4, L7)를 사용하는 방법과 HTTP 요청을 받아 분산이 가능한 웹서버 입니다. 어떤 방법을 사용하던지 복수개의 톰캣을 사용하면 상황에 따라 프로세스를 추가하여 처리하는 용량을 증가시킬 수 있습니다. 물론 앞서 장비의 추가 상황을 제외한다면 단일 프로세스 보다는 복수의 프로세스를 사용하여 부하를 분산시킬 수 있습니다.
톰캣은 웹서버의 역할을 함께 수행할 수 있는 기능을 동반하고 있습니다. 하지만 정적인 소스를 처리함에 있어서는 기존 웹서버의 처리 능력이 더 우월하기 때문에 소스 처리의 추체를 분산시켜 처리 속도를 증가시킬 수 있습니다. 대표적인 정적인 소스는 html, css, 이미지 파일 입니다. 앞서 요청의 분산으로 부하를 분산시키는 역할과 더불어 어플리케이션 소스 또한 처리추체를 분산시키고, 더불어 웹서버와 톰캣에서 좀더 빠르게 처리 할 수 있고, 처리 가능한 요청의 처리를 분담할 수 있습니다.
일반적으로 Failover로 표현하는 장애처리 및 장애극복은 복수의 톰캣 프로세스를 사용함에 따른 장점입니다. 특정 톰캣 프로세스에 장애가 발생하더라도 다른 톰캣 프로세스에서 요청을 처리하게 됨으로 단일 프로세스로 운영할때보다 서비스 지속성에 장점을 갖습니다.
아파치가 설치되었다는 가정하에 톰캣을 연동하는 방법은 다음과 같습니다.
유닉스/리눅스/맥의 경우 mod_jk의 소스를 다운받아 아파치의 apxs
와 함께 컴파일하는 과정이 필요합니다. 윈도우에서도 컴파일하여 사용할 수 있으나 비쥬얼 스튜디오가 있어야 컴파일을 할 수 있기 때문에 별도의 바이너리 파일로 제공됩니다. 다운로드페이지에서 플랫폼에 맞는 모듈을 다운받습니다.
유닉스/리눅스/맥의 경우 컴파일을 수행하기위해 아파치의 'apxs'가 필요합니다. 다운받은 소스 압축파일을 풀고 다음과 같이 컴파일 합니다.
$ tar xvfz tomcat-connectors-1.2.40-src.tar.gz
+$ cd ~/Downloads/tomcat-connectors-1.2.40-src/native
+$ ./configure —with-apxs=$APACHE_HOME_DIR/bin/apxs
+$ make
+$ make install
+
컴파일이 완료된 모듈은 자동으로 $APACHE_HOME/modules/mod_jk.so
로 생성됩니다. 윈도우에서는 다운 받은 바이너리 모듈의 압축을 풀어 동일한 디렉토리에 복사하면 됩니다.
생성된 모듈을 아파치에서 사용할 수 있도록 설정하는 작업을 합니다. $APACHE_HOME/conf/httpd.conf
에 설정하거나 별도의 conf
파일을 생성하여 읽게 하여도 됩니다. httpd.conf
에 설정하는 내용은 다음과 같습니다.
LoadModule jk_module modules/mod_jk.so
+
+<IfModule jk_module>
+ JkWorkersFile conf/workers.properties
+ JkLogFile logs/mod_jk.log
+ JkLogLevel info
+ JkMountFile conf/uri.properties
+</IfModule>
+
JkWorkersFile
은 요청을 처리하는 워커, 즉 톰캣을 정의하는 파일을 지정합니다.
JkMountFile
은 워커와 워커가 처리할 요청을 맵핑하는 파일을 지정합니다. JkMount
만으로도 설정이 가능하나 하나의 파일에서 별도로 관리하기 위해서는 해당 파일을 지정하는 것을 권장합니다. httpd.conf
에 JkMount
를 사용하는 경우 다음과 같이 정의할 수 있습니다.
#jsp 파일을 worker1 워커가 처리하는 경우
+JkMount /*.jsp worker1
+
+#server 경로의 요청을 worker2 워커가 처리하는 경우
+JkMount /servlet/* worker2
+
워커는 그 단어의 의미에서도 추측할 수 있듯이 mod_jk에서 지정하는 요청을 처리하는 대상, 즉 톰켓 프로세스를 의미합니다. 워커는 다음과 같은 설정 방식을 따릅니다.
worker.[WORKER_NAME].[TYPE]=[VALUE]
+
설정의 예는 다음과 같습니다.
worker.properties
의 설정 예제는 다음과 같습니다.
해당 설정은 LB로 구성되는 워커를 정의합니다. LB로 구성될 worker1
과 worker2
를 정의합니다.
worker1
은 192.168.0.10
의 ip와 8009
포트로 ajp13
형태의 요청을 받아들이며 lbfactor
는 1입니다.worker2
은 192.168.0.11
의 ip와 8009
포트로 ajp13
형태의 요청을 받아들이며 lbfactor
는 1입니다.loadbalancer
는 LB를 수행하기 위한 워커로 lb
워커 형태입니다.lb
형태의 워커는 LB 대상 워커를 balace_workers
를 정의하여 나열합니다. 예제에서는 worker1
과 worker2
가 대상으로 지정되었습니다.worker1
과 worker2
는 lbfactor
가 같기 때문에 같은 비율로 요청이 전달됩니다.worker.list
에 지정합니다. 예제에서는 loadbalancer
워커를 지정하였습니다.워커의 정의로 요청을 처리할 워커가 준비되었다면 어떤 요청을 전달할지 정의해햐 합니다. 앞서 JkMount
를 사용한 방식은 간단히 설명하였고 여기서는 uri.properties
파일에서 별도로 요청의 처리 맵핑을 관리하도록 하였습니다. JkMountFile
로 지정되는 이 설정 파일은 다음과 같은 설정 방식을 따릅니다.
[URL or FILE_EXTENSION]=[WORKER or WORKER_LIST]
+
이렇게 설정되는 설정 파일의 내용의 예는 다음과 같습니다.
/*.do=worker1
+/*.jsp=worker2
+/servlet/*=loadbalancer
+
JkMount
와 표현방식에 약간의 차이('='의 사용여부)가 있음에 주의하여 설정합니다.
설정이 완료되면 아파치 프로세스를 재기동 합니다. 이후 맵핑한 요청설정에 따라 아파치에 요청을 합니다. jsp파일을 톰캣이 처리하도록 설정하였다면 톰캣에서 요청해보고 url을 아파치로 변경하여 동일하게 요청되는지 확인합니다.
웹서버와 연동하는 주요 기능중 한가지는 장애처리입니다. 일반적으로는 이런 장애처리 동작시 기존 처리중이던 HTTP Session 정보는 장애가 발생한 톰캣이 가지고 있었기 때문에 없어지게 됩니다. 이같은 현상은 기존에 로그인하여 작업을 하던 중 해당 톰캣 프로세스에 문제가 발생하여 다른 톰캣 프로세스로 요청이 넘어가면 로그인 하던 세션이 끊겨 다시금 작업을 수행하는 현상이 발생하는 것을 예로 들수 있습니다.
톰캣에서는 장애처리시의 HTTP Session을 복구하여 지속적인 세션의 유지를 가능하게 하고자 '클러스터' 기능을 제공합니다. 클러스터는 Multicast로 톰캣 프로세스간에 클러스터를 형성하고 멤버로 구성된 톰캣간에 세션을 공유하는 방식입니다.
기능의 활성화는 단순히 server.xml
의 Cluster
디스크립터의 주석을 해제하는 것만으로도 가능합니다.
하지만 동일 장비에서 기동되는 톰캣간이나 서비스가 다른 톰캣이 여럿 기동중인 경우에는 설정값들이 중복되어 톰캣 기동이나 서비스 처리시 문제가 발생할 수 있습니다. 따라서 기본적인 설정 값 외에 별도의 설정들을 적용해야 하는 경우 server.xml
에서 클러스터를 사용하기 위한 디스크립터 위에 설명한 도큐먼트의 내용을 참고해야 합니다.
만약 도큐먼트의 설정들이 너무 많거나 어떻게 적용해야 하는지 이해하기 힘든경우 톰캣 5.5버전의 server.xml
을 참고하시기 바랍니다. 해당 버전에서는 6.0 이후 단순히 한줄로 적용된 Cluster
디스크립터와는 다르게 기본적인 설정과 값이 같이 적용되어 있습니다. 아래 예제는 도큐먼트의 기본 설정에서 가져온 내용입니다.
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"
+ channelSendOptions="8">
+
+ <Manager className="org.apache.catalina.ha.session.DeltaManager"
+ expireSessionsOnShutdown="false"
+ notifyListenersOnReplication="true"/>
+
+ <Channel className="org.apache.catalina.tribes.group.GroupChannel">
+ <Membership className="org.apache.catalina.tribes.membership.McastService"
+ address="228.0.0.4"
+ port="45564"
+ frequency="500"
+ dropTime="3000"/>
+ <Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
+ address="auto"
+ port="4000"
+ autoBind="100"
+ selectorTimeout="5000"
+ maxThreads="6"/>
+
+ <Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter">
+ <Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/>
+ </Sender>
+ <Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/>
+ <Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatch15Interceptor"/>
+ </Channel>
+
+ <Valve className="org.apache.catalina.ha.tcp.ReplicationValve"
+ filter=""/>
+ <Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve"/>
+
+ <Deployer className="org.apache.catalina.ha.deploy.FarmWarDeployer"
+ tempDir="/tmp/war-temp/"
+ deployDir="/tmp/war-deploy/"
+ watchDir="/tmp/war-listen/"
+ watchEnabled="false"/>
+
+ <ClusterListener className="org.apache.catalina.ha.session.JvmRouteSessionIDBinderListener"/>
+ <ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/>
+</Cluster>
+
앞서 여러개의 톰캣 클러스터를 사용하는 경우 Membership
과 Reciver
디스크립터의 내용에 주의합니다. Membership
은 동일한 설정을 갖는 톰캣 끼리 같은 클러스터 멤버 그룹으로 인지하는 내용으로 멀티캐스트 통신을 수행합니다. Membershop
의 address
와 port
가 동일한 톰캣 프로세스 끼지 클러스터 기능을 수행합니다. Reciver
는 클러스터간의 메시지를 주고받는 역할을 수행하며 TCP 통신을 수행합니다. 따라서 동일한 장비의 톰캣은 Reciver
에서 설정되는 port
에 차이가 있어야 합니다.
설정된 톰캣 클러스터의 기능은 어플리케이션이 세션 복제를 허용하는가의 여부에 따라 동작하게 됩니다. 따라서 어플리케이션의 web.xml
에 복제가능을 활성화하는 디스크립터를 추가합니다.
<web-app>
+ ...
+ <distributeable/>
+ ...
+</web-app>
+
복제 설정이 추가된 어플리케이션이 배치된 톰캣은 기동시 클러스터를 활성화하고 멤버간에 통신을 수행하는 메시지가 로그에 나타납니다.
구성된 클러스터와 어플리케이션은 LB로 구성되어 요청하며 각 톰캣 프로세스는 세션을 공유하기 때문에 하나의 톰캣 프로세스가 종료되더라도 다른 톰캣 프로세스에서 세션을 받아 수행하는 것을 확인할 수 잇습니다.
`,6);function w(x,f){const s=p("ExternalLinkIcon");return c(),l("div",null,[i,u,d,k,a("p",null,[n("웹서버는 프록시 기능만을 사용하여도 톰캣과의 연동이 가능하나 톰캣으로의 연동을 좀더 긴밀하게 하기 위해 별도의 모듈을 제공합니다. 이는 "),m,n("로 제공되는데 "),a("a",g,[n("http://tomcat.apache.org"),t(s)]),n("의 Document와 Download에서 확인할 수 있습니다. 연동가능한 대표적인 웹서버로는 다음의 웹서버와 모듈이 요구됩니다.")]),h,a("p",null,[n("아파치(Apache HTTP Server)는 가장 많이 사용되고 모든 플랫폼을 지원하는 대표적인 웹서버로서 이번 장에서 설명하고자하는 웹서버와의 연동에서 사용하고자 합니다. 기타 웹서버의 경우 톰캣의 "),a("a",v,[n("토큐먼트"),t(s)]),n(" 내용을 참고하시기 바랍니다.")]),b,a("p",null,[a("a",_,[n("http://tomcat.apache.org/tomcat-7.0-doc/cluster-howto.html: 기본 설정"),t(s)])]),q])}const j=o(r,[["render",w],["__file","08-webserver.html.vue"]]),S=JSON.parse('{"path":"/05-Software/Tomcat/tomcat101/08-webserver.html","title":"8. Tomcat 웹서버 연동","lang":"ko-KR","frontmatter":{"description":"Tomcat","tag":["Tomcat","Java"],"head":[["meta",{"property":"og:url","content":"https://docmoa.github.io/05-Software/Tomcat/tomcat101/08-webserver.html"}],["meta",{"property":"og:site_name","content":"docmoa"}],["meta",{"property":"og:title","content":"8. Tomcat 웹서버 연동"}],["meta",{"property":"og:description","content":"Tomcat"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://raw.githubusercontent.com/Great-Stone/images/master/uPic/workerproperties.jpg?token=ADUAZXKQI5JMPI7G5L7PVNS67EUS4"}],["meta",{"property":"og:locale","content":"ko-KR"}],["meta",{"property":"og:updated_time","content":"2023-09-18T13:12:54.000Z"}],["meta",{"name":"twitter:card","content":"summary_large_image"}],["meta",{"name":"twitter:image:alt","content":"8. Tomcat 웹서버 연동"}],["meta",{"property":"article:tag","content":"Tomcat"}],["meta",{"property":"article:tag","content":"Java"}],["meta",{"property":"article:modified_time","content":"2023-09-18T13:12:54.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"8. Tomcat 웹서버 연동\\",\\"image\\":[\\"https://raw.githubusercontent.com/Great-Stone/images/master/uPic/workerproperties.jpg?token=ADUAZXKQI5JMPI7G5L7PVNS67EUS4\\",\\"https://raw.githubusercontent.com/Great-Stone/great-stone.github.io/master/assets/img/Tomcat_youtube/tomcatCluster.png\\",\\"https://raw.githubusercontent.com/Great-Stone/great-stone.github.io/master/assets/img/Tomcat_youtube/distributeable.png\\"],\\"dateModified\\":\\"2023-09-18T13:12:54.000Z\\",\\"author\\":[]}"]]},"headers":[{"level":2,"title":"8.1 웹서버 연동의 이유","slug":"_8-1-웹서버-연동의-이유","link":"#_8-1-웹서버-연동의-이유","children":[{"level":3,"title":"8.1.1 요청분산","slug":"_8-1-1-요청분산","link":"#_8-1-1-요청분산","children":[]},{"level":3,"title":"8.1.2 소스분산","slug":"_8-1-2-소스분산","link":"#_8-1-2-소스분산","children":[]},{"level":3,"title":"8.1.3 장애극복","slug":"_8-1-3-장애극복","link":"#_8-1-3-장애극복","children":[]}]},{"level":2,"title":"8.2 mod_jk","slug":"_8-2-mod-jk","link":"#_8-2-mod-jk","children":[{"level":3,"title":"8.2.1 mod_jk 다운로드","slug":"_8-2-1-mod-jk-다운로드","link":"#_8-2-1-mod-jk-다운로드","children":[]},{"level":3,"title":"8.2.2 모듈 컴파일","slug":"_8-2-2-모듈-컴파일","link":"#_8-2-2-모듈-컴파일","children":[]},{"level":3,"title":"8.2.3 모듈 설정","slug":"_8-2-3-모듈-설정","link":"#_8-2-3-모듈-설정","children":[]},{"level":3,"title":"8.2.4 워커 정의","slug":"_8-2-4-워커-정의","link":"#_8-2-4-워커-정의","children":[]},{"level":3,"title":"8.2.5 처리할 요청의 정의","slug":"_8-2-5-처리할-요청의-정의","link":"#_8-2-5-처리할-요청의-정의","children":[]}]},{"level":2,"title":"8.2.6 테스트","slug":"_8-2-6-테스트","link":"#_8-2-6-테스트","children":[]},{"level":2,"title":"8.3 클러스터","slug":"_8-3-클러스터","link":"#_8-3-클러스터","children":[]}],"git":{"createdTime":1640327880000,"updatedTime":1695042774000,"contributors":[{"name":"Administrator","email":"admin@example.com","commits":2},{"name":"Great-Stone","email":"hahohh@gmail.com","commits":1}]},"readingTime":{"minutes":1.51,"words":452},"filePathRelative":"05-Software/Tomcat/tomcat101/08-webserver.md","localizedDate":"2021년 12월 24일","excerpt":"\\n톰캣 단일로 서비스하는 경우도 있지만 일반적으로 웹서버와 연동하여 사용하는 경우가 보다 더 많습니다. 그 이유를 다음과 같이 정리합니다.
"}');export{j as comp,S as data}; diff --git a/assets/09-security.html-CSGyCK8z.js b/assets/09-security.html-CSGyCK8z.js new file mode 100644 index 0000000000..898c19f438 --- /dev/null +++ b/assets/09-security.html-CSGyCK8z.js @@ -0,0 +1,18 @@ +import{_ as t,a as e,b as n,c as l,d as i,e as s,f as a}from"./1564543591394-BKf8t37i.js";import{_ as d}from"./plugin-vue_export-helper-DlAUqK2U.js";import{o,c as r,e as c}from"./app-Bzk8Nrll.js";const p={},u=c(`사용자별 배포수행을 위한 사용자 설정을 설명합니다.
Jenkins 관리
로 이동하여 Configure Global Security
를 클릭합니다.Enable security
는 보안 설정 여부를 설정하는 항목으로 기본적으로는 비활성화되어있습니다. 체크하여 활성화하면 다양한 보안 옵션을 설정할 수 있는 항목이 표기 됩니다.
Security Realm 에서는 Jenkins에서 사용하는 사용자 관리 방식을 선택합니다.
사용자의 가입 허용
이 활성화되면 Jenkins 에 접속하는 사용자는 스스로 계정을 생성하고 접근 가능합니다.Authorization 에서는 사용자 권한에 대한 설정을 정의합니다.
1.164
이전 버전의 동작과 동일하게 관리됩니다. Admin
사용자만 모든 기능을 수행하며, 일반 사용자와 비로그인 사용자는 읽기만 가능합니다.다음은 권한 매트릭스의 항목과 권한별 설명입니다.
항목 | 권한 | 의미 |
---|---|---|
Overall | Administer | 시스템의 전역 설정을 변경할 수 있다. OS 에서 허용된 범위안에서 전체 시스템 엑세스드의 매우 민감한 설정을 수행 |
Read | 젠킨스의 모든 페이지 확인 가능 | |
RunScripts | 그루비 콘솔이나 그루비 CLI 명령을 통해 그루비 스크립트를 실행 | |
UploadPlugins | 특정 플러그인을 업로드 | |
ConfigureUpdateCenter | 업데이트 사이트와 프록시 설정 | |
Slave | Configure | 기존 슬레이브 설정 가능 |
Delete | 기존 슬레이브 삭제 | |
Create | 신규 슬레이브 생성 | |
Disconnect | 슬레이브 연결을 끊거나 슬레이브를 임시로 오프라인으로 표시 | |
Connect | 슬레이브와 연결하거나 슬레이브를 온라인으로 표시 | |
Job | Create | 새로운 작업 생성 |
Delete | 기존 작업 삭제 | |
Configure | 기존 작업의 설정 갱신 | |
Read | 프로젝트 설정에 읽기 전용 권한 부여 | |
Discover | 익명 사용자가 작업을 볼 권한이 없으면 에러 메시지 표시를 하지 않고 로그인 폼으로 전환 | |
Build | 새로운 빌드 시작 | |
Workspace | 젠킨스 빌드를 실행 하기 위해 체크아웃 한 작업 영역의 내용을 가져오기 가능 | |
Cancel | 실행중인 빌드 취소 | |
Run | Delete | 빌드 내역에서 특정 빌드 삭제 |
Update | 빌드의 설명과 기타 프로퍼티 수정(빌드 실패 사유등) | |
View | Create | 새로운 뷰 생성 |
Delete | 기존 뷰 삭제 | |
Configure | 기존 뷰 설정 갱신 | |
Read | 기존 뷰 보기 | |
SCM | Tag | 특정 빌드와 관련된 소스 관리 시스템에 태깅을 생성 |
CSRF Protection 항목에 있는 Prevent Cross Site Request Forgery exploits
항목은 페이지마다 nonce 또는 crumb 이라 불리우는 임시 값을 삽입하여 사이트 간 요청 위조 공격을 막을 수 있게 해줍니다. 사용방법은 위에서 REST API 에 대한 설명 시 crumb 값을 얻고, 사용하는 방법을 참고합니다.
Jenkins에서 Pipeline을 설정하는 경우 일부 보안적인 값이 필요한 경우가 있습니다. 예를 들면 Username
과 Password
같은 값입니다. 앞서의 과정에서 Credentials
를 생성하는 작업을 일부 수행해 보았습니다. 여기서는 생성된 인증 값을 Pipeline에 적용하는 방법을 설명합니다.
Pipeline
타입의 Item을 추가로 생성합니다. (e.g. 09-02.SecuringSecretCredentialsAndFiles) 설정은 다음과 같이 수행합니다.
Pipeline
스크립트에 다음과 같이 입력 합니다.
pipeline {
+ agent any
+ environment {
+ SECRET=credentials('jenkins-secret-text')
+ }
+ stages {
+ stage('Build') {
+ steps {
+ echo "\${env.SECRET}"
+ }
+ }
+ }
+}
+
저장 후 Build Now
를 클릭하여 빌드를 수행하면 실패하게 되고 Console Output
에서 진행사항을 보면, Pipeline 스크립트에서 선언한 jenkins-secret-text
때문에 에러가 발생한 것을 확인할 수 있습니다.
좌측 상단의 Jenkins
버튼을 클릭하여 최상위 메뉴로 이동합니다.
좌측 메뉴의 Credentials
를 클릭하고 (global)
도메인을 클릭합니다.
좌측에 Add Credentials
를 클릭하여 새로운 항목을 추가합니다.
저장 후 다시 빌드를 수행하면 정상적으로 수행됩니다. 해당 값은 숨기기 위한 값이므로 Pipeline 스크립트에서 echo
로 호출하더라도 ****
이란 값으로 표기 됩니다.
이같은 방법은 Password같은 보안에 민감한 정보를 사용하기에 유용합니다.
Jenkins의 변화와 활동에 대한 감시를 위한 설정 방법을 설명합니다. Jenkins에 새로운 플러그인을 추가하고 설정합니다.
Jenkins 관리
로 이동하여 플러그인 관리
를 클릭합니다.설치 가능
탭을 클릭하고 상단의 검색에 audit
를 입력하면 Audit Trail
플러그인이 나타납니다. 선택하여 설치합니다.Jenkins 관리
로 이동하여 시스템 설정
을 클릭합니다.ADD LOGGER
드롭박스에서 Log File
을 선택하여 설정합니다. 저장 후 빌드나 Job의 설정 변경등의 작업을 수행하면, audit.log.0
으로 지정된 파일 경로에 생성됨을 확인 할 수 있습니다.
$ tail -f ./audit.log.0
+Jul 31, 2019 10:47:32,727 AM job/02-02.Jobs/ #12 Started by user GyuSeok.Lee
+Jul 31, 2019 10:47:42,738 AM /job/03-04.WebhookBuild Triggering/configSubmit by gyulee
+Jul 31, 2019 10:48:09,001 AM /configSubmit by gyulee
+
다양한 프로젝트를 관리하는 경우 관리상, 빌드 프로젝트를 관리해야할 필요성이 발생합니다. Jenkins에서 Forder 아이템을 생성하여 관리 편의성과 보안요소를 추가할 수 있습니다.
우선 테스트를 위한 사용자를 추가합니다.
Jenkins 관리
를 클릭하여 Manage Users
로 이동합니다.사용자 생성
을 클릭하여 새로운 사용자를 추가합니다. 다음으로 Forder 타임의 Item을 추가합니다.
새로운 Item
을 클릭하여 이름을 02-Project
로 예를 들어 지정하고, Forder를 클릭하여 OK
버튼을 클릭합니다.SAVE
버튼을 클릭하고 좌측 상단의 Jenkins
버튼을 클릭하여 최상위 페이지로 이동합니다.02-02.Jobs
에 마우스를 대면 드롭박스 메뉴를 확장할 수 있습니다. Move
를 클릭합니다.Jenkins >> 02-Project
를 선택하고 MOVE
버튼을 클릭합니다. 다시 최상위 메뉴로 오면 02-02.Jobs
가 사라진 것을 확인할 수 있습니다. 02
로 시작하는 다은 프로젝트도 같은 작업을 수행하여 이동시킵니다.02-Project
를 클릭하면 이동된 프로젝트들이 나타납니다.권한 설정을 하여 현재 Admin 권한의 사용자는 접근 가능하고 새로 생성한 tester는 접근불가하도록 설정합니다.
Folder에 접근하는 권한을 설정하기위해 Jenkins 관리
의 Configure Global Security
로 이동합니다.
Authorization항목의 Project-based Matrix Authorization Strategy
를 선택합니다.
ADD USER OR GROUP...
을 클릭하여 Admin 권한의 사용자를 추가합니다.
Admin 권한의 사용자에게는 모든 권한을 주고 Authenticated Users
에는 Overall의 Read
권한만 부여합니다.
생성한 02-Project
로 이동하여 좌측 메뉴의 Configure
를 클릭합니다.
Properties에 추가된 Enable project-based security
를 확성화하면 항목별 권한 관리 메트릭스가 표시됩니다. Job의 Build, Read, ViewStatus, Workspace를 클릭하고 View의 Read를 클릭하여 권한을 부여합니다.
로그아웃 후에 앞서 추가한 test
사용자로 로그인 하면 기본적으로 다른 프로젝트나 Item들은 권한이 없기 때문에 보이지 않고, 앞서 설정한 02-Project
폴더만 리스트에 나타납니다.
Jenkins의 인증 기능을 사용하여 보안적 요소를 구성할 수 있습니다. Audit 로그를 활용하여 사용자별 활동을 기록할 수도 있고 Folder를 활용하면 간단히 사용자/그룹에 프로젝트를 구분하여 사용할 수 있도록 구성할 수 있습니다.
',32),g=[u];function y(m,f){return o(),r("div",null,g)}const b=d(p,[["render",y],["__file","09-security.html.vue"]]),v=JSON.parse('{"path":"/05-Software/Jenkins/pipeline101/09-security.html","title":"9. Security","lang":"ko-KR","frontmatter":{"description":"jenkins 101","tag":["cicd","jenkins"],"head":[["meta",{"property":"og:url","content":"https://docmoa.github.io/05-Software/Jenkins/pipeline101/09-security.html"}],["meta",{"property":"og:site_name","content":"docmoa"}],["meta",{"property":"og:title","content":"9. Security"}],["meta",{"property":"og:description","content":"jenkins 101"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"ko-KR"}],["meta",{"property":"og:updated_time","content":"2023-09-18T13:12:54.000Z"}],["meta",{"property":"article:tag","content":"cicd"}],["meta",{"property":"article:tag","content":"jenkins"}],["meta",{"property":"article:modified_time","content":"2023-09-18T13:12:54.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"9. Security\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2023-09-18T13:12:54.000Z\\",\\"author\\":[]}"]]},"headers":[{"level":2,"title":"9.1 Securing your deployment with users","slug":"_9-1-securing-your-deployment-with-users","link":"#_9-1-securing-your-deployment-with-users","children":[]},{"level":2,"title":"9.2 Securing secret credentials and files","slug":"_9-2-securing-secret-credentials-and-files","link":"#_9-2-securing-secret-credentials-and-files","children":[]},{"level":2,"title":"9.3 Auditing your environment","slug":"_9-3-auditing-your-environment","link":"#_9-3-auditing-your-environment","children":[]},{"level":2,"title":"9.4 Using forders to create security realms","slug":"_9-4-using-forders-to-create-security-realms","link":"#_9-4-using-forders-to-create-security-realms","children":[]}],"git":{"createdTime":1640327880000,"updatedTime":1695042774000,"contributors":[{"name":"Administrator","email":"admin@example.com","commits":1},{"name":"Great-Stone","email":"hahohh@gmail.com","commits":1}]},"readingTime":{"minutes":1.44,"words":433},"filePathRelative":"05-Software/Jenkins/pipeline101/09-security.md","localizedDate":"2021년 12월 24일","excerpt":"\\n사용자별 배포수행을 위한 사용자 설정을 설명합니다.
\\nJenkins 관리
로 이동하여 Configure Global Security
를 클릭합니다.Enable security
는 보안 설정 여부를 설정하는 항목으로 기본적으로는 비활성화되어있습니다. 체크하여 활성화하면 다양한 보안 옵션을 설정할 수 있는 항목이 표기 됩니다.
Thread는 JVM내에 요청된 작업을 동시에 처리하기 위한 작은 cpu라고 볼 수 있습니다. 톰캣에 서비스 처리를 요청하는 경우 해당 요청은 Queue에 쌓여 FIFO로 Thread에 전달되고 Thread에 여유가 있는 경우 Queue에 들어온 요청은 바로 Thread로 전달되어 Queue Length
는 0을 유지하지만 Thread가 모두 사용중이여서 더이상의 요청 처리를 하지 못하는 경우 새로 발생한 요청은 Queue에 쌓이면서 지연이 발생합니다.
Thread가 많을수록 동시에 많은 요청을 처리하기 때문에 작은 Thread 수는 서비스를 지연시키지만 이에 반해 Thread도 자원을 소모하므로 필요이상의 큰 값은 불필요한 JVM의 자원을 소모하게 되고 하나의 프로세스 내의 Thread 수는 톰캣 기준으로 700개 이하로 설정할 것을 권장합니다.
사실상 요청은 지연이 최소화 되어야 하며 지연이 길어질수록 Thread를 점유하여 동시간대에 사용가능한 Thread 수를 줄이므로 적정한 Thread 개수의 설정 상태에서 요청을 더 많이 받고자 한다면 지연에 대한 문제점을 찾는 것을 우선해야 합니다.
쓰레드는 Connector
기준으로 생성됩니다. 따라서 HTTP나 AJP, SSL이 설정된 Connector
마다 다른 쓰레드 수를 설정할 수 있습니다. 또는 하나의 쓰레드 풀을 생성하고 Connector
에서 해당 쓰레드 풀의 쓰레드를 같이 사용하도록 설정할 수도 있습니다.
기본적인 'Connector'는 다음과 같이 설정되어있습니다.
<Connector port="8080" protocol="HTTP/1.1"
+ connectionTimeout="20000" redirectPort="8443" />
+
+<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
+
HTTP나 AJP 프로토콜이 정의된 Connector
는 설정되어 있지는 않지만 기본값으로 최대 쓰레드 200개의 설정을 가지고 있습니다. 쓰레드 관련 설정값은 다음과 같습니다.
Attribute | Description |
---|---|
maxSpareThreads | Idle 상태로 유지할 max thread pool size |
maxThreads | 동시 요청에 의해 Connector가 생성 할 수 있는 최대 request 수 |
minSpareThreads | tomcat을 실행할때 최소로 유지할 Idle Thread 수 |
maxIdleTime | Thread를 유지하는 시간(ms) |
이런 설정 값들로 다시금 정의하면 기본 Connector
를 다음과 같이 설정할 수 있습니다.
<Connector port="8080" protocol="HTTP/1.1"
+ connectionTimeout="20000" redirectPort="8443"
+ maxSpareThreads="5"
+ maxThreads="15"
+ minSpareThreads="10" />
+
+<Connector port="8009" protocol="AJP/1.3" redirectPort="8443"
+ maxSpareThreads="5"
+ maxThreads="15"
+ minSpareThreads="10" />
+
Executor
디스크립터는 Connector
의 쓰레드 설정에 별도의 실행자로 설정하여 동일한 Executor
를 사용하는 Connector
는 같은 쓰레드 풀에서 쓰레드를 사용하도록 하는 기능입니다.
별도의 Connector
를 사용하여 서비스하지만 모두 같은 쓰레드 자원을 사용하기 위함이며 connector
에 executor
라는 설정을 사용하여 공통의 쓰레드 풀을 이용할 수 있습니다. tomcatThreadPool
이라는 이름을 갖는 Executor
와 각 Connector
에 설정하는 예는 다음과 같습니다.
<Executor name="tomcatThreadPool"
+ namePrefix="catalina-exec-"
+ maxThreads="150"
+ minSpareThreads="4"/>
+
+<Connector executor="tomcatThreadPool"
+ port="8080" protocol="HTTP/1.1"
+ connectionTimeout="20000" redirectPort="8443"/>
+
+<Connector executor="tomcatThreadPool"
+ port="8009" protocol="AJP/1.3" redirectPort="8443"/>
+
Executor
에는 name
을 정의하여 다른 Connector
에서 해당 Executor
를 정의할 수 있는 연결고리를 만듭니다. 그리고 쓰레드의 이름을 정의하는 namePrifix
설정으로 다른 쓰레드와 구분할 수 있도록 합니다. 기존 Connector
에 설정하던 쓰레드 관련 설정을 Excutor
에 함으로서 Connector
에 공통 쓰레드 풀을 제공합니다.
쓰레드 덤프는 실행중인 Thread의 종류와 시작점, 실행한 클래스와 메소드 순서, 현재 상태등을 기록하는 JVM의 고유 기능입니다. 쓰레드 덤프로 서비스의 흐름과 서비스 지연시 수행중인 작업, 병목등을 확인할 수 있습니다.
쓰레드 덤프의 시작에는 쓰레드 이름과 쓰레드의 정보가 기록되며 이후 쓰레드 상태에 대해 설명합니다.
트레이스의 읽는 순서는 위가 최근 실행한 클래스와 메소드이기 때문에 아래서부터 위로 읽습니다.
쓰레드 덤프를 발생시키는 법은 다음과 같습니다.
프로세스 ID를 확인
유닉스/리눅스/맥
ps -ef | grep java
+
JDK 5+
$JAVA_HOME/bin/jps
+
쓰레드 덤프 발생
유닉스/리눅스/맥
kill -3 <pid>
+
JDK 5+
$JAVA_HOME/bin/jstack <pid>
+
쓰레드 덤프 확인
catalina.out
파일 확인Thread는 JVM내에 요청된 작업을 동시에 처리하기 위한 작은 cpu라고 볼 수 있습니다. 톰캣에 서비스 처리를 요청하는 경우 해당 요청은 Queue에 쌓여 FIFO로 Thread에 전달되고 Thread에 여유가 있는 경우 Queue에 들어온 요청은 바로 Thread로 전달되어 Queue Length
는 0을 유지하지만 Thread가 모두 사용중이여서 더이상의 요청 처리를 하지 못하는 경우 새로 발생한 요청은 Queue에 쌓이면서 지연이 발생합니다.
Vault Secrets Operator(이하, VSO)를 사용하면 파드가 쿠버네티스 시크릿에서 기본적으로 볼트 시크릿을 사용할 수 있다.
VSO는 지원되는 Custom Resource Definitions (CRD) 집합의 변경 사항을 감시하여 작동한다. 각 CRD는 오퍼레이터가 Vault Secrets을 Kubernetes Secret에 동기화할 수 있도록 하는 데 필요한 사양(specification)을 제공한다.
오퍼레이터는 소스(source) 볼트 시크릿 데이터를 대상(destination) 쿠버네티스 시크릿에 직접 쓰며, 소스에 대한 변경 사항이 수명 기간 동안 대상에 복제되도록 한다. 이러한 방식으로 애플리케이션은 대상 시크릿에 대한 접근 권한만 있으면 그 안에 포함된 시크릿 데이터를 사용할 수 있다.
VSO가 지원하는 기능은 다음과 같다:
',7),v=n("li",null,"모든 Vault 비밀 엔진 지원.",-1),k=n("li",null,"Vault와의 TLS/mTLS 통신.",-1),h={href:"https://developer.hashicorp.com/vault/docs/auth/kubernetes",target:"_blank",rel:"noopener noreferrer"},b=n("code",null,"Pod",-1),y=n("code",null,"ServiceAccount",-1),g=t("Deployment
, ReplicaSet
, StatefulSet
쿠버네티스 리소스 유형에 대한 시크릿 로테이션.Helm
, Kustomize
참고:
현재, 오퍼레이터는 쿠버네티스 인증 방법만 지원한다. 시간이 지남에 따라 더 많은 Vault 인증 방식에 대한 지원을 추가할 예정이다.
Vault 연결 및 인증 구성은 VaultConnection
및 VaultAuth
CRD에서 제공한다. 이는 모든 비밀 복제 유형 리소스가 참조하는 기본 사용자 지정 리소스로 간주할 수 있다.
VaultConnection
커스텀 리소스오퍼레이터가 단일 Vault 서버 인스턴스에 연결하는 데 필요한 구성을 제공한다.
---
+apiVersion: secrets.hashicorp.com/v1alpha1
+kind: VaultConnection
+metadata:
+ namespace: vso-example
+ name: example
+spec:
+ # 필수적인 구성정보
+ # Vault 서버 주소
+ address: http://vault.vault.svc.cluster.local:8200
+
+ # 선택적인 구성정보
+ # 모든 Vault 요청에 포함될 HTTP headers
+ # headers: []
+
+ # TLS 연결의 SNI 호스트로 사용할 TLS 서버 이름
+ # tlsServerName: ""
+
+ # Vault에 대한 TLS 연결에 대한 TLS verification을 건너뜀
+ # skipTLSVerify: false
+
+ # Kubernetes Secret에 저장된 신뢰할 수 있는 PEM 인코딩된 CA 인증서 체인
+ # caCertSecretRef: ""
+
오퍼레이터가 VaultConnection
사용자 지정 리소스에 지정된 대로 단일 Vault 서버 인스턴스에 인증하는 데 필요한 구성을 제공한다.
---
+apiVersion: secrets.hashicorp.com/v1alpha1
+kind: VaultAuth
+metadata:
+ namespace: vso-example
+ name: example
+spec:
+ # 필수적인 구성정보
+ # 해당 VaultConnection 커스텀 리소스의 VaultConnectionRef
+ # 값을 지정하지 않으면 오퍼레이터는 자체 쿠버네티스 네임스페이스에 구성된
+ # 'default' VaultConnection을 기본값으로 사용
+ vaultConnectionRef: example
+
+ # Vault에 인증할 때 사용하는 방법.
+ method: kubernetes
+ # Auth methods로 인증할 때 사용할 마운트.
+ mount: kubernetes
+ # 쿠버네티스용 인증 구성을 사용하려면, Method를 쿠버네티스로 설정해야 함
+ kubernetes:
+ # Vault에 인증할 때 사용할 역할
+ role: example
+ # Vault에 인증할 때 사용할 서비스어카운트 파드/애플리케이션당 항상 고유한 서비스어카운트를 제공을 권장
+ serviceAccount: default
+
+ # 선택적인 구성정보
+ # 인증 백엔드가 마운트되는 Vault 네임스페이스(Vault Enterprise 전용기능)
+ # namespace: ""
+
+ # Vault에 인증할 때 사용할 매개변수
+ # params: []
+
+ # 모든 Vault 인증 요청에 포함할 HTTP 헤더
+ # headers: []
+
오퍼레이터가 단일 볼트 시크릿을 단일 쿠버네티스 시크릿으로 복제하는 데 필요한 구성을 제공한다. 지원되는 각 CRD는 아래 문서에 설명된 Vault Secret의 Class에 특화되어 있다.
VaultStaticSecret
사용자 지정 리소스오퍼레이터가 단일 볼트 정적 시크릿을 단일 Kubernetes Secret에 동기화하는 데 필요한 구성을 제공한다.
`,14),x={href:"https://developer.hashicorp.com/vault/docs/secrets/kv/kv-v2",target:"_blank",rel:"noopener noreferrer"},C={href:"https://developer.hashicorp.com/vault/docs/secrets/kv/kv-v1",target:"_blank",rel:"noopener noreferrer"},w=t(`---
+apiVersion: secrets.hashicorp.com/v1alpha1
+kind: VaultStaticSecret
+metadata:
+ namespace: vso-example
+ name: example
+spec:
+ vaultAuthRef: example
+ mount: kvv2
+ type: kv-v2
+ name: secret
+ refreshAfter: 60s
+ destination:
+ create: true
+ name: static-secret1
+
VaultPKISecret
사용자 지정 리소스운영자가 단일 볼트 PKI 시크릿을 단일 쿠버네티스 시크릿에 동기화하는 데 필요한 구성을 제공한다.
---
+apiVersion: secrets.hashicorp.com/v1alpha1
+kind: VaultPKISecret
+metadata:
+ namespace: vso-example
+ name: example
+spec:
+ vaultAuthRef: example
+ mount: pki
+ name: default
+ commonName: example.com
+ format: pem
+ expiryOffset: 1s
+ ttl: 60s
+ namespace: tenant-1
+ destination:
+ create: true
+ name: pki1
+
오퍼레이터가 단일 볼트 동적 시크릿을 단일 쿠버네티스 시크릿에 동기화하는 데 필요한 구성을 제공한다.
`,7),T={href:"https://developer.hashicorp.com/vault/docs/secrets/databases",target:"_blank",rel:"noopener noreferrer"},K={href:"https://developer.hashicorp.com/vault/docs/secrets/aws",target:"_blank",rel:"noopener noreferrer"},R={href:"https://developer.hashicorp.com/vault/docs/secrets/azure",target:"_blank",rel:"noopener noreferrer"},q={href:"https://developer.hashicorp.com/vault/docs/secrets/gcp",target:"_blank",rel:"noopener noreferrer"},O=t(`---
+apiVersion: secrets.hashicorp.com/v1alpha1
+kind: VaultDynamicSecret
+metadata:
+ namespace: vso-example
+ name: example
+spec:
+ vaultAuthRef: example
+ mount: db
+ role: postgres
+ destination:
+ create: true
+ name: dynamic1
+
\\n\\n"}');export{D as comp,P as data}; diff --git a/assets/10-artifacts.html-Bs2kQ5kz.js b/assets/10-artifacts.html-Bs2kQ5kz.js new file mode 100644 index 0000000000..1b0649097a --- /dev/null +++ b/assets/10-artifacts.html-Bs2kQ5kz.js @@ -0,0 +1,31 @@ +import{_ as n,a,b as t,c as s}from"./1564546697375-DhhAeZD5.js";import{_ as e}from"./plugin-vue_export-helper-DlAUqK2U.js";import{o as i,c,e as p}from"./app-Bzk8Nrll.js";const o={},l=p(`참고:
\\n
\\n현재 Vault 비밀 오퍼레이터는 공개 베타 버전입니다. *here*에서 GitHub 이슈를 개설하여 피드백을 제공해 주세요.
빌드 이후 빌드의 결과를 기록하고 저장하는 방법을 설명합니다.
Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 10-01.CreatingAndStoringArtifacts)
Pipeline에 다음과 같이 스크립트를 추가합니다.
pipeline {
+ agent any
+ stages{
+ stage('Build') {
+ steps{
+ sh 'echo "Generating artifacts for \${BUILD_NUMBER}" > output.txt'
+ }
+ }
+ stage('Archive') {
+ steps {
+ archiveArtifacts artifacts: 'output.txt', onlyIfSuccessful: true
+ }
+ }
+ }
+}
+
Archive
Stage에 archiveArtifacts
스크립트가 동작하는 예제입니다. 이같은 Pipeline 스크립트 작성을 도와주는 툴을 추가로 확인해 봅니다.
Pipeline Syntax
링크를 클릭합니다.Sample Step
에서 archiveArtifacts: Archive the artifacts
를 선택합니다. 고급...
을 클릭합니다.GENERATE PIPELINE SCRIPT
를 클릭합니다.결과물을 확인하면 Pipeline 스크립트에 작성한 형태와 같은 것을 확인 할 수 있습니다.
좌측 메뉴의 Build Now
를 클릭하여 빌드 수행 후에 화면에 Artifacts 항목이 추가된 것을 확인할 수 있습니다. UI 상에는 마지막 빌드 결과가 강조되어 나오고 각 빌드에 대한 결과물은 각각의 빌드단계의 다운로드 버튼으로 확인하고 다운로드 할 수 있습니다.
빌드 이후 보관되는 파일에 대해 어떤 프로젝트, 어떤 빌드 에서 발생한 결과물인지 확인할 수 있는 핑거프린팅 기능을 설명합니다.
Step 1
의 프로젝트를 그대로 사용하거나 Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 10-02.FingerprintingForArtifactTracking)
Step 1
Pipeline 스크립트의 archiveArtifacts
에 fingerprint: true
를 추가합니다.
pipeline {
+ agent any
+ stages{
+ stage('Build') {
+ steps{
+ sh 'echo "Generating text artifacts: Build:\${BUILD_NUMBER}" > output.txt'
+ }
+ }
+ stage('Archive') {
+ steps {
+ archiveArtifacts artifacts: 'output.txt', fingerprint: true, onlyIfSuccessful: true
+ }
+ }
+ }
+}
+
파일의 지문을 확인합니다.
첫번째 빌드를 수행하고 빌드 결과 아카이브 파일 output.txt
파일을 다운로드 받습니다. (파일을 우클릭하고 다른 이름으로 링크 저장...
or Download Linked File
을 클릭하여 파일을 받습니다.)
좌측 상단의 Jenkins
를 클릭하여 최상위 메뉴로 돌아갑니다.
좌측 메뉴의 파일 핑거프린트 확인
을 클릭합니다.
파일 선택
버튼을 클릭하여 앞서 다운로드한 파일을 선택하고 확인하기
버튼을 클릭합니다.
어떤 프로젝트의 몇번째 빌드에서 발생한 파일인지 확인합니다.
두번째 빌드를 수행하고 파일 핑거프린트를 확인해 봅니다.
빌드 번호 정보가 변경된 것을 확인합니다.
빌드 이후 빌드의 결과를 기록하고 저장하는 방법을 설명합니다.
\\nPipeline 타입의 Item을 추가로 생성합니다. (e.g. 10-01.CreatingAndStoringArtifacts)
\\nPipeline에 다음과 같이 스크립트를 추가합니다.
\\npipeline {\\n agent any\\n stages{\\n stage('Build') {\\n steps{\\n sh 'echo \\"Generating artifacts for \${BUILD_NUMBER}\\" > output.txt'\\n }\\n }\\n stage('Archive') {\\n steps {\\n archiveArtifacts artifacts: 'output.txt', onlyIfSuccessful: true\\n }\\n }\\n }\\n}\\n
무엇인가에 대한 모니터링은 그 대상의 상태를 확인하기 위함입니다. 문제가 있는지, 어떤 동작을 하고 있는지, 알아야 할 내용이 있다면 그 사항을 알수 있도록 하는, 즉 대상의 상태를 감시하는 것입니다.
톰캣을 사용하여 서비스를 제공하는 입장에서는 톰캣의 상태를 감시할 수 있어야 합니다. 톰캣의 작업 상태나 자원의 상태, 특정 문제 상황이 발생하는 징조를 파악하는 것입니다. 모니터링을 잘 수행하면 더나은 서비스와 서비스 장애로 인한 손실을 예방할 수 있습니다.
톰캣에는 기본적으로 제공하는 모니터링 툴이 있습니다. 자세하지는 않더라도 필요한 만큼의 정보를 제공합니다.
앞서 어플리케이션의 배치를 통해 알아보았던 Manager APP
에서는 다음의 정보를 확인 할 수 있습니다. 만약 호스트가 여러개라면 해당 Manager APP
어플리케이션을 별도로 배치하여 해당 호스트의 배치 정보를 확인 할 수 있습니다.
Manager APP
에서의 수행 결과 메시지그리고 상단의 Server Status
링크를 통해 이동하면 다음의 정보를 확인 할 수 있습니다.
배치된 어플리케이션의 Session의 숫자에 링크된 페이지에서는 현재 생성된 세션의 정보와 해당 세션을 강제 종료시킬 수 있는 Sessoin Administration
을 제공합니다.
host manager
에서는 다음의 정보를 확인 할 수 있습니다.
톰캣 5.5 버전에서는 admin
어플리케이션이 제공됩니다. 다운로드의 아카이브 사이트에서 확인 가능하며 톰캣을 다운받기위한 버전하위에 apache-tomcat-x.x.xx-admin
이름을 갖는 파일이 있습니다. 여타 WAS에서 제공되는 만큼의 웹 콘솔 UI를 제공하는 관리 툴로서 톰캣 내에 설정과 각 항목의 정보를 파악할 수 있습니다. 다만 톰캣 5.5 이후로는 제공되지 않습니다.
psi-probe
는 예전에 lambda probe
였으나 현재 구글에서 관리하기 시작한 후로 명칭이 변경되었습니다.
psi-probe
의 공식 웹 사이트의 다운로드 항목에서 파일을 받아 압축을 풀면 probe.war
웹 어플리케이션이 있습니다. 해당 어플리케이션을 톰캣에 배치하면 psi-probe
를 실행할 수 있으며 tomcat-user.xml
에 manager
권한을 갖는 사용자로 접근하게 됩니다.
앞서 텍스트로만 표현되던 정보들도 보다 보기좋게 제공하고 각 자원이나 설정을 파악하는데 있어서 기본 톰캣 모니터링 툴보다 나은 기능을 제공합니다. 다만 일부 모니터링 항목은 5.5까지를 지원하고 톰캣 8.0에 대한 지원이 불가능하며 2013년 3월 이후로 업데이트가 없다는 점이 단점입니다.
jkstatus는 mod_jk
를 사용하여 연동한 경우 아파치에서 확인할 수 있는 톰켓 연동에 대한 모니터링 툴 입니다.
사용을 위해서 worker.properties
에 status
워커를 추가합니다.
worker.list=tomcat1,tomcat2,loadbalancer,status
+...
+worker.status.type=status
+
그리고 uri.properties
에 요청을 수행할 경로를 워커에 맵핑합니다.
...
+/jkstatus=status
+
설정 후 아파치를 재기동하면 아파치 요청 url의 컨텍스트에 설정한 요청 경로를 입력하여 'jkstatus' 툴을 확인 할 수 있습니다. 앞서 80으로 요청하는 아파치의 경우 다음과 같이 요청 할 수 있습니다.
`,9),b={href:"http://tomcat.apache.org/connectors-doc/reference/status.html",target:"_blank",rel:"noopener noreferrer"},f=o('jkstatus로 확인할 수 있는 정보는 다음과 같습니다.
JDK에는 $JAVA_HOMe/bin/jvisualvm
에 실행파일이 위치합니다.
visualVM의 장점중 하나는 플러그인 입니다. 현재까지도 상당수의 플러그인이 제공되고 있으며 플러그인 API가 공개되어 있어 원하는 모니터링 플러그인을 생성할 수도 있습니다.
자바 프로세스는 자체적으로 로컬환경에서는 visualVM에 자동으로 인지되게 됩니다. 따라서 수행중인 JVM 프로세스는 visualVM의 Local
항목에 감지되어 목록에 나타납니다. 그리고 원격지의 JVM 프로세스 또한 JMX(Java Monitoring Extension)을 통해 로컬의 visualVM에서 모니터링 할 수 있습니다. 서비스로 등록된 로컬의 톰캣은 프로세스가 보이지 않기 때문에 리모트로 구성하는 방법을 따릅니다. 톰캣의 리모트 구성 방법은 두가지가 있습니다.
Java 기본 JMX 설정
Java에서는 옵션을 통해 JMX를 활성화하고 설정 할 수 있습니다. 스크립트에 다음의 JMX의 옵션을 설정합니다.
#setenv.sh
+CATALINA_OPTS="-Dcom.sun.management.jmxremote
+-Dcom.sun.management.jmxremote.port=18080
+-Dcom.sun.management.jmxremote.ssl=false
+-Dcom.sun.management.jmxremote.authenticate=false"
+
이 후 visualVM의 'Remote'에 플랫폼 IP를 등록하고 우클릭을 하면 'Add JMX connection...'을 통해 원격지의 톰캣을 등록할 수 있습니다.
jmx remote 모듈
톰캣에서는 jmx를 위한 모듈을 제공합니다. 톰캣의 다운로드에 보면 Extra
항목에 Remote JMX jar
가 있습니다.
$CATALINA_HOME/lib/catalina-jmx-remote.jar
에 위치시킵니다.$CATALINA_HOME/common/lib/catalina-jmx-remote.jar
에 위치시킵니다.Java에서는 옵션을 통해 JMX를 활성화하고 설정 할 수 있습니다. 스크립트에 다음의 JMX의 옵션을 설정합니다.
#setenv.sh
+CATALINA_OPTS="-Dcom.sun.management.jmxremote.ssl=false
+-Dcom.sun.management.jmxremote.authenticate=false"
+
그리고 server.xml
에 Listener
디스크립터로 JmxRemoteLifecycleListener
를 추가합니다.
<Server port="8005" shutdown=“SHUTDOWN">
+
+ <Listener className="org.apache.catalina.mbeans.JmxRemoteLifecycleListener"
+ rmiRegistryPortPlatform="10001" rmiServerPortPlatform="10002" />
+
설정된 톰캣은 visualVM의 Remote 등록시 다음의 정보로 접근 정보를 생성합니다.service:jmx:rmi://192.168.56.101:10002/jndi/rmi://192.168.56.101:10001/jmxrmi
로컬이나 리모트에 설정된 톰캣 프로세스는 좌측의 네비게이션에 나타나며 해당 항목을 더블클릭하여 우측 화면에서 모니터링 하게 됩니다.
visualVM에서는 기본적으로 다음의 기능을 제공합니다.
이외에도 플러그인에서 제공하는 기능을 활용한 다양한 모니터링이 가능합니다.
Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 11-01.AutomatingDeploymentWithPipelines)
Pipeline에 다음과 같은 스크립트를 입력합니다.
pipeline {
+ agent any
+ stages {
+ stage('Build') {
+ steps {
+ sh 'echo "Hello World"'
+ }
+ }
+ stage('Test') {
+ steps {
+ sh 'echo "Test Hello World!"'
+ }
+ }
+ }
+}
+
두개의 Stage를 갖는 Pipeline 스크립트입니다. Pipeline은 빌드 수행시의 각 단계를 구분하여 빌드의 과정을 확인하고 실패에 따른 단계별 확인이 가능합니다.
좌측 Build Now
를 클릭하여 빌드를 수행하면 빌드에 대한 결과는 Stage 별로 성공 실패의 여부와 로그를 확인할 수 있도록 Stage View
가 UI로 제공됩니다. Stage 별로 Stage View는 기록되며, Stage에 변경이 있거나 이름이 변경되는 경우에는 해당 UI에 변경이 발생하여 기존 Pipeline 기록을 보지 못할 수 있습니다.
Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 11-02.CreatingPipelineGates)
Pipeline에 다음과 같은 스크립트를 입력합니다.
pipeline {
+ agent any
+ stages {
+ stage('Build') {
+ steps {
+ sh 'echo "Hello World"'
+ }
+ }
+ stage('BuildMore'){
+ steps {
+ input message: "Shall we build more?"
+ sh '''
+ echo "We are approved; continue!"
+ ls -lah
+ '''
+ }
+ }
+ }
+}
+
개의 Stage를 갖는 Pipeline 스크립트입니다. 두번째 Stage에 input
스크립트가 있습니다. 이 스크립트가 추가되면 Pipeline을 진행하면서 해당하는 동작을 수행할 것인지, 마치 승인 작업과 같은 동작을 수행할 수 있습니다.
좌측 Build Now
를 클릭하여 빌드를 수행하면 두번째 Stage에서 해당 작업을 수행할 지에 대한 물음을 확인 할 수 있습니다.
Abort
를 선택하면 빌드 취소와 같은 동작으로 실패로 처리되지는 않습니다.
빌드 단계를 구현할 때 Pipeline 스크립트로 하나의 프로젝트 내에서 모든 동작을 정의 할 수도 있지만 서로다른 Job을 연계하고, 승인 절차를 따르도록 구성할 수 있습니다.
Job promotion 기능을 사용하기 위한 플러그인을 설치합니다.
Jenkins 관리
에서 플러그인 관리
를 선택합니다.설치 가능
탭을 클릭하고 상단의 검색에 promoted
를 입력하면 promoted builds
를 확인 할 수 있습니다. 설치합니다.FreeStyle 타입의 Item을 생성합니다. (e.g. 11-03.Job-one)
General 탭의 Promote builds when...
를 활성화 하여 설정합니다.
Only when manually approved
활성화 ADD PRAMETER
드롭박스에서 Boolean Parameter
를 선택합니다. Build 드롭박스에서 Execute shell
을 선택합니다.
다음을 입력합니다.
echo 'This is the Job-one'
+
저장하면 생성된 프로젝트에 Promotion Status
항목이 추가되어 생성됩니다.
11-03.Job-one
빌드 후 승인에 대한 다음 빌드를 진행할 FreeStyle 타입의 Item을 생성합니다. (e.g. 11-03.Job-two)
빌드 유발 항목에서 Build when another project is promoted
를 활성화 합니다. 어떤 Job에서 promote 상황이 발생하였을 때 빌드를 수행할지 지정합니다.
Build 드롭박스에서 Execute shell
을 선택합니다.
다음을 입력합니다.
echo 'This is the Job-two'
+
11-03.Job-one
에 대한 빌드를 수행합니다. 수행 완료 후 빌드 히스토리의 최근 빌드를 클릭(e.g. #1)하면 Promotion Status
에 승인절차를 기다리고 있음을 확인할 수 있습니다. Parameters 항목의 approve
를 체크하고 APPROVE
버튼을 클릭합니다.
승인이 완료되면 해당 프로젝트의 승인에 대한 이벤트를 통해 빌드를 수행하는 11-03.Job-two
가 이어서 빌드됨을 확인 할 수 있습니다.
SCM의 Multibranch를 빌드하는 과정에 대해 설명합니다.
다음의 GitHub repository를 fork 합니다.
',30),h={href:"https://github.com/Great-Stone/multibranch-demo",target:"_blank",rel:"noopener noreferrer"},y=a('Multibranch Pipeline 형태의 Item을 생성합니다. (e.g. 11-04.MultibranchRepositoryAutomation)
ADD SOURCE
드롭박스에서 GitHub를 클릭합니다. VALIDATE
버튼을 클릭하여 잘 접근 되는지 확인합니다.Periodically if not otherwise run
를 활성화 합니다. 1 minute
으로 설정합니다.저장 후에는 자동적으로 모든 브랜치의 소스를 빌드 수행합니다.
SCM에서 브랜치를 여러개 관리하고 모두 빌드와 테스팅이 필요하다면 Multibranch 프로젝트를 생성하여 등록하고, 빌드 관리가 가능합니다.
Pipeline 을 스크립트를 작성하는 방법을 배워봅니다. Pipeline 타입의 Item을 생성합니다. (e.g. 11-05. CreatingPipelineWithSnippets)
Pipeline에 다음과 같은 스크립트를 입력합니다.
pipeline {
+ agent any
+ stages {
+ stage("Hello") {
+ steps {
+ echo 'Hello World'
+ }
+ }
+ }
+}
+
echo가 동작할때 시간을 기록하도록 스크립트를 수정해보겠습니다.
Pipeline Syntax 링크를 클릭합니다.
Sample Step에서 timestamps: timestamps
를 선택하고 GENERATE PIPELINE SCRIPT
버튼을 클릭합니다.
timestamps {
+ // some block
+}
+
사용방식을 확인하고 앞서 Pipeline 스크립트의 stage에 시간을 기록하도록 수정합니다.
...
+stage("Hello") {
+ steps {
+ timestamps {
+ echo 'Hello World'
+ }
+ }
+}
+...
+
빌드를 수행하고 로그를 확인해 봅니다. echo 동작이 수행 될때 시간이 함께 표기되는 것을 확인 할 수 있습니다.
Pipeline에서 사용할 수 있는 변수를 확인하고 사용하는 방법을 알아봅니다. Pipeline 타입의 Item을 생성합니다. (e.g. 11-06.DiscoveringGlobalPipelineVariables)
Pipeline에 다음과 같은 스크립트를 입력합니다.
pipeline {
+ agent any
+ stages {
+ stage('Build') {
+ steps {
+ echo "We are in build \${currentBuild.number}"
+ echo "Our current result is \${currentBuild.currentResult}"
+ }
+ }
+ stage('BuildMore'){
+ steps {
+ echo "Name of the project is \${currentBuild.projectName}"
+ }
+ }
+ stage('BuildEnv'){
+ steps {
+ echo "Jenkins Home : \${env.JENKINS_HOME}"
+ }
+ }
+ }
+}
+
Pipeline 스크립트에서 사용가능한 변수와 사용방법은 Pipeline Syntax
링크의 Global Variables Reference
항목에서 확인 가능합니다.
Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 11-01.AutomatingDeploymentWithPipelines)
\\nPipeline에 다음과 같은 스크립트를 입력합니다.
\\npipeline {\\n agent any\\n stages {\\n stage('Build') {\\n steps {\\n sh 'echo \\"Hello World\\"'\\n }\\n }\\n stage('Test') {\\n steps {\\n sh 'echo \\"Test Hello World!\\"'\\n }\\n }\\n }\\n}\\n
하나의 장비에는 둘 이상의 톰캣을 운영하는 경우가 발생합니다. 이경우 설정 파일을 별도로 생성하고 스크립트를 통해 해당 설정을 읽게 하는 식의 방법을 사용할 수도 있지만 기본 제공되는 스크립트가 수정되는 양이 많을수록 관리의 난이도가 증가합니다. 따라서 톰캣의 프로세스를 여러개 기동하기 위한 방법으로 권장하는 것을 $CATALINA_HOME
을 단순히 여러개 만드는 것입니다. 톰캣은 그 자체 용량이 그리 크지 않기 때문에 여러개 복사한다하여도 큰 무리 없이 사용가능한 가벼운 엔진 입니다. 따라서 설정 파일을 추가로 구성하고 스크립트를 수정하는 방법 보다는 기본 톰캣 엔진 전체를 복사하는 것을 권장합니다.
앞서 옵션을 적용함에 있어 강조한 setenv를 이야기 하고자 합니다. 톰캣에 대한 서비스를 지원하다보면 주로 catalina.sh(bat)
를 수정하는 경우가 대부분이고 현재가지는 운영중인 서비스에 setenv
사용한 사례찾기는 힘든것 같습니다. 하지만 catalina.sh(bat)
스크립트에서도 명시하듯 해당 스크립트를 수정하는 것은 설정한다는 이유 외에는 단점이 더 많기 때문에 반드시 setenv
를 통해 추가적인 스크립트 추가 설정을 권장합니다.
Unix/Linux/Mac 플랫폼에서 톰캣이 별도의 계정으로 구성되어 있지만 간혹 root 계정으로 실수로 기동하는 경우가 발생합니다. 톰캣에서는 server.xml
에 다음의 설정으로 root 로의 기동을 방지 할 수 있습니다.
<Server port="8005" shutdown="SHUTDOWN">
+ <Listener className="org.apache.catalina.security.SecurityListener" checkedOsUsers="root" />
+ ...
+
이러한 Listener
디스크립터의 설정으로 root 계정의 실행을 방지하며, 만약 root로 기동하는 경우 다음과 같은 메시지를 발생시킵니다.
java.lang.Error: Start attempted while running as user [root]. Running Tomcat as this user has been blocked by the Lifecycle listener org.apache.catalina.security.SecurityListener (usually configured in CATALINA_BASE/conf/server.xml)
+
Connector
디스크립터로 정의되는 프로토콜에 대한 정의는 톰캣이 요청을 받아들이는 통로를 설정하는 것이기 때문에 주요 설정 중 하나 입니다. 앞서 살펴본 쓰레드 설정외에도 도움이 될만한 옵션에 대한 내용은 다음과 같습니다.
옵션 | 기능 설명 |
---|---|
acceptCount="10" | request Queue의 길이를 정의 : idle thread가 없으면 queue에서 idle thread가 생길때 까지 요청을 대기하는 queue의 길이 : 요청을 처리할 수 없는 상황이면 빨리 에러 코드를 클라이언트에게 보내서 에러처리 표시 |
enableLookups="false" | Servlet/JSP 코드 중에서 들어오는 http request에 대한 ip를 조회 하는 명령등이 있을 경우 DNS 이름을 IP주소로 바꾸기 위해서 DNS 서버에 look up 요청을 보냄 : 서버간의 round trip 발생을 막을 수 있음 |
compression="off" | HTTP message body를 gzip 형태로 압축해서 리턴하지 않음 |
maxConnection="8192" | 하나의 톰캣인스턴스가 유지할 수 있는 Connection의 수를 정의 : 현재 연결되어 있는 실제 Connection의 수가 아니라 현재 사용중인 socket fd (file descriptor)의 수 |
maxKeepAliveRequest="1" | HTTP 1.1 Keep Alive Connection을 사용할 때, 최대 유지할 Connection 수를 결정하는 옵션 : Keep Alive를 사용할 환경이 아닌 경우에 설정 |
tcpNoDelay="true" | TCP 프로토콜은 기본적으로 패킷을 보낼때 바로 보내지 않음 : 버퍼사이즈에 데이터가 모두 담길때까지 패킷 전송을 보류함으로 대기 시간이 발생하는 것을 방지 : 트래픽이 증가하지만 현 망 속도를 고려하였을 때 문제가 크지 않음 |
GitHub를 SCM으로 사용하는 경우 다음과 같은 메시지가 출력되면서 진행되지 않는 경우가 있습니다.
GitHub API Usage: Current quota has 5000 remaining (447380 over budget). Next quota of 5000 in 5 days 0 hr. Sleeping for 4 days 23 hr.
+14:07:33 GitHub API Usage: The quota may have been refreshed earlier than expected, rechecking...
+
이 경우 서버 시간과 GitHub의 시간이 맞지 않아 발생할 수 있는 이슈 입니다. ntpdate를 재설정 합니다.
`,5),p=e("li",null,[e("p",null,"RHEL7 : ntpd를 재시작 합니다."),e("div",{class:"language-bash","data-ext":"sh","data-title":"sh"},[e("pre",{class:"language-bash"},[e("code",null,`$ systemctl restart ntpd +`)])])],-1),h=e("br",null,null,-1),m={href:"https://access.redhat.com/solutions/4130881",target:"_blank",rel:"noopener noreferrer"},u=e("div",{class:"language-bash","data-ext":"sh","data-title":"sh"},[e("pre",{class:"language-bash"},[e("code",null,[t(`$ systemctl stop chronyd +$ chronyd `),e("span",{class:"token parameter variable"},"-q"),t(` +$ systemctl start chronyd +`)])])],-1),g=e("h2",{id:"유용한-플러그인",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#유용한-플러그인"},[e("span",null,"유용한 플러그인")])],-1),x=e("ul",null,[e("li",null,"Restart Safely : Jenkins를 재기동해야하는 경우 빌드가 수행중이지 않을 때 자동으로 Restart 시켜줍니다. 설치 후에는 왼쪽 주 메뉴에 표시됩니다."),e("li",null,"ThinBackup : Jenkins의 구성을 백업, 복구할 수 있는 기능을 제공합니다. 백업 주기나 백업 개수등을 정의 할 수 있습니다.")],-1);function _(b,y){const a=i("ExternalLinkIcon");return o(),s("div",null,[d,e("ul",null,[p,e("li",null,[e("p",null,[t("RHEL8 : RHEL8에서는 ntpdate를 사용하지 않고 chronyd가 대신합니다."),h,e("a",m,[t("https://access.redhat.com/solutions/4130881"),r(a)])]),u])]),g,x])}const v=n(c,[["render",_],["__file","12-appendix.html.vue"]]),S=JSON.parse('{"path":"/05-Software/Jenkins/pipeline101/12-appendix.html","title":"Apendix","lang":"ko-KR","frontmatter":{"description":"jenkins 101","tag":["cicd","jenkins"],"head":[["meta",{"property":"og:url","content":"https://docmoa.github.io/05-Software/Jenkins/pipeline101/12-appendix.html"}],["meta",{"property":"og:site_name","content":"docmoa"}],["meta",{"property":"og:title","content":"Apendix"}],["meta",{"property":"og:description","content":"jenkins 101"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"ko-KR"}],["meta",{"property":"og:updated_time","content":"2023-09-18T13:12:54.000Z"}],["meta",{"property":"article:tag","content":"cicd"}],["meta",{"property":"article:tag","content":"jenkins"}],["meta",{"property":"article:modified_time","content":"2023-09-18T13:12:54.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Apendix\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2023-09-18T13:12:54.000Z\\",\\"author\\":[]}"]]},"headers":[{"level":2,"title":"GitHub SCM 연동 이슈","slug":"github-scm-연동-이슈","link":"#github-scm-연동-이슈","children":[]},{"level":2,"title":"유용한 플러그인","slug":"유용한-플러그인","link":"#유용한-플러그인","children":[]}],"git":{"createdTime":1640327880000,"updatedTime":1695042774000,"contributors":[{"name":"Administrator","email":"admin@example.com","commits":1},{"name":"Great-Stone","email":"hahohh@gmail.com","commits":1}]},"readingTime":{"minutes":0.31,"words":93},"filePathRelative":"05-Software/Jenkins/pipeline101/12-appendix.md","localizedDate":"2021년 12월 24일","excerpt":"\\nGitHub를 SCM으로 사용하는 경우 다음과 같은 메시지가 출력되면서 진행되지 않는 경우가 있습니다.
\\nGitHub API Usage: Current quota has 5000 remaining (447380 over budget). Next quota of 5000 in 5 days 0 hr. Sleeping for 4 days 23 hr.\\n14:07:33 GitHub API Usage: The quota may have been refreshed earlier than expected, rechecking...\\n
Update at 31 Jul, 2019
Jenkins Pipeline 을 구성하기 위해 VM 환경에서 Jenkins와 관련 Echo System을 구성합니다. 각 Product의 버전은 문서를 작성하는 시점에서의 최신 버전을 위주로 다운로드 및 설치되었습니다. 구성 기반 환경 및 버전은 필요에 따라 변경 가능합니다.
Category | Name | Version |
---|---|---|
VM | VirtualBox | 6.0.10 |
OS | Red Hat Enterprise Linux | 8.0.0 |
JDK | Red Hat OpenJDK | 1.8.222 |
Jenkins | Jenkins rpm | 2.176.2 |
Jenkins 실행 및 구성
Jenkins를 실행 및 구성하기위한 OS와 JDK가 준비되었다는 가정 하에 진행합니다. 필요 JDK 버전 정보는 다음과 같습니다.
필요 JDK를 설치합니다.
$ subscription-manager repos --enable=rhel-8-for-x86_64-baseos-rpms --enable=rhel-8-for-x86_64-appstream-rpms
+
+### Java JDK 8 ###
+$ yum -y install java-1.8.0-openjdk-devel
+
+### Check JDK version ###
+$ java -version
+openjdk version "1.8.0_222"
+OpenJDK Runtime Environment (build 1.8.0_222-b10)
+OpenJDK 64-Bit Server VM (build 25.222-b10, mixed mode)
+
Red Hatsu/Fedora/CentOS 환경에서의 Jenkins 다운로드 및 실행은 다음의 과정을 수행합니다.
`,11),an={href:"https://pkg.jenkins.io/redhat-stable/",target:"_blank",rel:"noopener noreferrer"},tn=a(`repository를 등록합니다.
$ sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo
+$ sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key
+
작성일 기준 LTS 버전인 2.176.2
버전을 설치합니다.
$ yum -y install jenkins
+
패키지로 설치된 Jenkins의 설정파일은 /etc/sysconfig/jenkins
에 있습니다. 해당 파일에서 실행시 활성화되는 포트 같은 설정을 변경할 수 있습니다.
## Type: integer(0:65535)
+## Default: 8080
+## ServiceRestart: jenkins
+#
+# Port Jenkins is listening on.
+# Set to -1 to disable
+#
+JENKINS_PORT="8080"
+
외부 접속을 위해 Jenkins에서 사용할 포트를 방화벽에서 열어줍니다.
$ firewall-cmd --permanent --add-port=8080/tcp
+$ firewall-cmd --reload
+
서비스를 부팅시 실행하도록 활성화하고 Jenkins를 시작합니다.
$ systemctl enable jenkins
+$ systemctl start jenkins
+
실행 후 브라우저로 접속하면 Jenkins가 준비중입니다. 준비가 끝나면 Unlock Jenkins
페이지가 나오고 /var/lib/jenkins/secrets/initialAdminPassword
의 값을 입력하는 과정을 설명합니다. 해당 파일에 있는 토큰 복사하여 붙여넣습니다.
이후 과정은 Install suggested plugins
를 클릭하여 기본 플러그인을 설치하여 진행합니다. 경우에 따라 Select plugins to install
을 선택하여 플러그인을 지정하여 설치할 수 있습니다.
플러그인 설치 과정을 선택하여 진행하면 Getting Started
화면으로 전환되어 플러그인 설치가 진행됩니다.
설치 후 기본 Admin User
를 생성하고, 접속 Url을 확인 후 설치과정을 종료합니다.
GitHub 계정생성
진행되는 실습에서는 일부 GitHub를 SCM으로 연동합니다. 원활한 진행을 위해 GitHub계정을 생성해주세요. 또는 별개의 Git 서버를 구축하여 사용할 수도 있습니다.
Jenkins Theme (Optional)
Jenkins는 간단히 테마와 회사 CI를 적용할 수 있는 플러그인이 제공됩니다.
Jenkins 관리
로 이동하여 플러그인 관리
를 클릭합니다.
설치 가능
탭을 클릭하고 상단의 검색에 theme
를 입력하면 Login Theme
와 Simple Theme
를 확인 할 수 있습니다. 둘 모두 설치합니다.
로그아웃을 하면 로그인 페이지가 변경된 것을 확인 할 수 있습니다.
기본 Jenkins 테마를 변경하기 위해서는 다음의 과정을 수행합니다.
`,17),ln={href:"http://afonsof.com/jenkins-material-theme/",target:"_blank",rel:"noopener noreferrer"},on=a("Build your own theme with a company logo!
에서 색상과 로고를 업로드 합니다.
DOWNLOAD YOUR THEME!
버튼을 클릭하면 CSS파일이 다운됩니다.
Jenkins 관리
로 이동하여 시스템 설정
를 클릭합니다.
Theme
항목의 Theme elements
의 드롭다운 항목에서 Extra CSS
를 클릭하고 앞서 다운받은 CSS파일의 내용을 붙여넣고 설정을 저장하면 적용된 테마를 확인할 수 있습니다.
Delivery vs Deployment
Jenkins for CI/CD
Job and Project
프로젝트는 Job의 일부 입니다. 즉, 모든 프로젝트가 Job이지만 모든 Job이 프로젝트는 아닙니다. Job의 구조는 다음과 같습니다.
',7),un=a(`FreeStyleProejct, MatrixProject, ExternalJob만 New job
에 표시됩니다.
Step 1. New pipeline
Step 1에서는 stage
없이 기본 Pipeline을 실행하여 수행 테스트를 합니다.
Jenkins 로그인
좌측 새로운 Item
클릭
Enter an item name
에 Job 이름 설정 (e.g. 2.Jobs)
Pipeline
선택 후 OK
버튼 클릭
Pipeline
항목 오른 쪽 Try sample Pipelie...
클릭하여 Hello world
클릭 후 저장
node {
+ echo 'Hello World'
+}
+
좌측 Build now
클릭
좌측 Build History
의 최근 빌드된 항목(e.g. #1) 우측에 마우스를 가져가면 dropdown 버튼이 생깁니다. 해당 버튼을 클릭하여 Console Output
클릭
수행된 echo
동작 출력을 확인합니다.
Started by user GyuSeok.Lee
+Running in Durability level: MAX_SURVIVABILITY
+[Pipeline] Start of Pipeline
+[Pipeline] node
+Running on Jenkins in /var/lib/jenkins/workspace/2.Jobs
+[Pipeline] {
+[Pipeline] echo
+Hello World
+[Pipeline] }
+[Pipeline] // node
+[Pipeline] End of Pipeline
+Finished: SUCCESS
+
Step 2. New pipeline
Step 2에서는 stage
를 구성하여 실행합니다.
기존 생성한 Job 클릭 (e.g. 02-02.Jobs)
좌측 구성
을 클릭하여 Pipeline
스크립트를수정합니다.
pipeline{
+ agent any
+ stages {
+ stage("Hello") {
+ steps {
+ echo 'Hello World'
+ }
+ }
+ }
+}
+
수정 후 좌측 Build Now
를 클릭하여 빌드 수행 후 결과를 확인합니다.
Step 1
에서의 결과와는 달리 Stage View
항목과 Pipeline stage가 수행된 결과를 확인할 수 있는 UI가 생성됩니다.
수행된 빌드의 Console Output
을 확인하면 앞서 Step 1
에서는 없던 stage 항목이 추가되어 수행됨을 확인 할 수 있습니다.
Started by user GyuSeok.Lee
+Running in Durability level: MAX_SURVIVABILITY
+[Pipeline] Start of Pipeline
+[Pipeline] node
+Running on Jenkins in /var/lib/jenkins/workspace/2.Jobs
+[Pipeline] {
+[Pipeline] stage
+[Pipeline] { (Hello)
+[Pipeline] echo
+Hello World
+[Pipeline] }
+[Pipeline] // stage
+[Pipeline] }
+[Pipeline] // node
+[Pipeline] End of Pipeline
+Finished: SUCCESS
+
Step 3. Parameterizing a job
Pipeline 내에서 사용되는 매개변수 정의를 확인해 봅니다. Pipeline 스크립트는 다음과 같습니다.
pipeline {
+ agent any
+ parameters {
+ string(name: 'Greeting', defaultValue: 'Hello', description: 'How should I greet the world?')
+ }
+ stages {
+ stage('Example') {
+ steps {
+ echo "\${params.Greeting} World!"
+ }
+ }
+ }
+}
+
parameters
항목내에 매개변수의 데이터 유형(e.g. string)을 정의합니다. name
은 값을 담고있는 변수이고 defaultValue
의 값을 반환합니다. Pipeline에 정의된 parameters
는 params
내에 정의 되므로 \${params.매개변수이름}
과 같은 형태로 호출 됩니다.
저장 후 다시 구성
을 확인하면 이 빌드는 매개변수가 있습니다
가 활성화 되고 내부에 추가된 매개변수 항목을 확인 할 수 있습니다.
이렇게 저장된 Pipeline Job은 매개변수를 외부로부터 받을 수 있습니다. 따라서 좌측의 기존 Build Now
는 build with Parameters
로 변경되었고, 이를 클릭하면 Greeting을 정의할 수 있는 UI가 나타납니다. 해당 매개변수를 재정의 하여 빌드를 수행할 수 있습니다.
Step 4. Creating multiple steps for a job
다중스텝을 위한 Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 02-04.MultiStep)
Pipeline에 다음과 같이 스크립트를 추가합니다.
pipeline {
+ agent any
+ stages {
+ stage('Build') {
+ steps {
+ sh 'echo "Hello World"'
+ sh '''
+ echo "Multiline shell steps works too"
+ ls -lah
+ '''
+ }
+ }
+ }
+}
+
'''
은 스크립트 정의 시 여러줄을 입력할 수 있도록 묶어주는 역할을 합니다. 해당 스크립트에서는 sh
로 구분된 스크립트 명령줄이 두번 수행됩니다.
실행되는 여러 스크립트의 수행을 stage
로 구분하기위해 기존 Pipeline 스크립트를 다음과 같이 수정합니다.
pipeline {
+ agent any
+ stages {
+ stage('Build-1') {
+ steps {
+ sh 'echo "Hello World"'
+ }
+ }
+ stage('Build-2') {
+ steps {
+ sh '''
+ echo "Multiline shell steps works too"
+ ls -lah
+ '''
+ }
+ }
+ }
+}
+
stage를 구분하였기 때문에 각 실행되는 sh
스크립트는 각 스테이지에서 한번씩 수행되며, 이는 빌드의 결과로 나타납니다.
Step 5. Adding scripts as a job step
Pipeline의 step을 추가하여 결과를 확인하는 과정을 설명합니다. 피보나치 수열을 수행하는 쉘 스크립트를 시간제한을 두어 수행하고 그 결과를 확인합니다.
',28),vn={href:"https://namu.wiki/w/%ED%94%BC%EB%B3%B4%EB%82%98%EC%B9%98%20%EC%88%98%EC%97%B4",target:"_blank",rel:"noopener noreferrer"},gn={href:"https://namu.wiki/w/%ED%94%BC%EB%B3%B4%EB%82%98%EC%B9%98",target:"_blank",rel:"noopener noreferrer"},kn=a(`$ mkdir -p /var/jenkins_home/scripts
+$ cd /var/jenkins_home/scripts
+$ vi ./fibonacci.sh
+#!/bin/bash
+N=\${1:-10}
+
+a=0
+b=1
+
+echo "The Fibonacci series is : "
+
+for (( i=0; i<N; i++ ))
+do
+ echo "$a"
+ sleep 2
+ fn=$((a + b))
+ a=$b
+ b=$fn
+done
+# End of for loop
+
+$ chown -R jenkins /var/jenkins_home/
+$ chmod +x /var/jenkins_home/scripts/fibonacci.sh
+
다중스텝을 위한 Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 02-05.AddingStep)
Pipeline에 다음과 같이 스크립트를 추가합니다.
pipeline {
+ agent any
+ stages {
+ stage('Deploy') {
+ steps {
+ timeout(time: 1, unit: 'MINUTES') {
+ sh '/var/jenkins_home/scripts/fibonacci.sh 5'
+ }
+ timeout(time: 1, unit: 'MINUTES') {
+ sh '/var/jenkins_home/scripts/fibonacci.sh 32'
+ }
+ }
+ }
+ }
+}
+
steps
에 스크립트를 timeout
이 감싸고 있으며, 각 스크립트의 제한시간은 1분입니다. 빌드를 수행하면 최종적으로는 aborted
, 즉 중단됨 상태가 되는데 그 이유는 빌드 기록에서 해당 빌드를 클릭하면 확인 가능합니다.
Build History
에서 최신 빌드를 클릭합니다.
좌측 Pipeline Steps
를 클릭하면 Pipeline 수행 스텝을 확인할 수 있습니다.
첫번째로 나타나는 /var/jenkins_home/scripts/fibonacci.sh 5
를 수행하는 Shell Script
의 콘솔창 버튼을 클릭하면 잘 수행되었음을 확인 할 수 있습니다.
두번째로 나타나는 /var/jenkins_home/scripts/fibonacci.sh 32
를 수행하는 Shell Script
의 콘솔창 버튼을 클릭하면 다음과 같이 중도에 프로세스를 중지한 것을 확인 할 수 있습니다.
+ /var/jenkins_home/scripts/fibonacci.sh 32
+The Fibonacci series is :
+0
+1
+1
+2
+3
+...
+317811
+514229
+Sending interrupt signal to process
+/var/jenkins_home/scripts/fibonacci.sh: line 16: 13543 Terminated sleep 2
+832040
+/var/lib/jenkins/workspace/02-05.AddingStep@tmp/durable-e44bb232/script.sh: line 1: 13109 Terminated /var/jenkins_home/scripts/fibonacci.sh 32
+script returned exit code 143
+
Step 1. Tracking build state
Pipeline이 수행되는 동작을 추적하는 과정을 확인합니다. 이를 이를 위한 Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 03-01.TrackingBuildState)
Pipeline에 다음과 같이 스크립트를 추가합니다.
pipeline {
+ agent any
+ stages {
+ stage('Deploy') {
+ steps {
+ timeout(time: 1, unit: 'MINUTES') {
+ sh 'for n in \`seq 1 10\`; do echo $n; sleep 1; done'
+ }
+ timeout(time: 1, unit: 'MINUTES') {
+ sh 'for n in \`seq 1 50\`; do echo $n; sleep 1; done'
+ }
+ }
+ }
+ }
+}
+
Build Now
를 클릭하여 빌드를 수행합니다. 그러면, 좌측의 Build History
에 새로운 기록이 생성되면서 동작 중인것을 확인 할 수 있습니다.
첫번째 방법은 앞서 확인한 Pipeline Steps
를 확인하는 것입니다. 다시한번 확인하는 방법을 설명합니다.
Build History
에서 최신 빌드를 클릭합니다.Pipeline Steps
를 클릭하면 Pipeline 수행 스텝을 확인할 수 있습니다.현재 수행중인 Pipeline이 어떤 단계가 수행중인지 각 스탭별로 확인할 수 있고 상태를 확인할 수 있습니다.
두번째 방법은 출력되는 콘솔 로그를 확인하는 것입니다. Jenkins에서 빌드를 수행하면 빌드 수행 스크립트가 내부에 임시적으로 생성되어 작업을 실행합니다. 이때 발생되는 로그는 Console Output
을 통해 거의 실시간으로 동작을 확인 할 수 있습니다.
Build History
에서 최신 빌드에 마우스 포인터를 가져가면 우측에 드롭박스가 생깁니다. 또는 해당 히스토리를 클릭합니다.Console Output
나 클릭된 빌드 히스토리 상태에서 Console Output
를 클릭하면 수행중인 콘솔상의 출력을 확인합니다.마지막으로는 Pipeline을 위한 UI인 BlueOcean
플러그인을 활용하는 방법입니다. Blue Ocean은 Pipeline에 알맞은 UI를 제공하며 수행 단계와 각 단게별 결과를 쉽게 확인할 수 있습니다.
Jenkins 관리
에서 플러그인 관리
를 선택합니다.설치 가능
탭에서 Blue Ocean
을 선택하여 재시작 없이 설치
를 클릭 합니다.Blue Ocean
플러그인만 선택하여 설치하더라도 관련 플러그인들이 함께 설치 진행됩니다.Blue Ocean
항목을 확인 할 수 있습니다.Step 2. Polling SCM for build triggering
Git SCM을 기반으로 Pipeline을 설정하는 과정을 설명합니다. 이를 이를 위한 Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 03-02.PollingSCMforBuildTriggering)
해당 과정을 수행하기 위해서는 다음의 구성이 필요합니다.
Jenkins가 구성된 호스트에 git 설치
$ yum -y install git
+
Jenkins 설정
Jenkins 관리
의 Global Tool Configuration
클릭Path to Git executable
칸에 Path 입력 (e.g. /usr/bin/git)Pipeline을 다음과 같이 설정합니다.
`,27),mn=n("li",null,"Definition : Pipeline script from SCM",-1),bn=n("li",null,"SCM : Git",-1),hn={href:"https://github.com/Great-Stone/jenkins-git",target:"_blank",rel:"noopener noreferrer"},fn=a(`추가로 빌드 트리거를 위한 설정을 합니다.
Build Triggers
의 Poll SCM
활성화
Schedule 등록
# min hour day month day_of_week
+* * * * *
+# will run every minute on the minute
+
Polling으로 인한 빌드 트리거가 동작하면 좌측 메뉴의 Polling Log
에서 상태 확인이 가능합니다.
1분마다 확인 하도록 되어있기 때문에 다시 Polling을 시도하지만 변경사항이 없는 경우에는 Polling Log에 No changes
메시지가 나타나고 빌드는 수행되지 않습니다.
Step 3. Connecting Jenkins to GitHub
GitHub를 통한 CI 과정을 설명합니다. WebHook의 설정과 Jenkins에 관련 설정은 어떻게 하는지 알아봅니다.
Jenkins에서 접속가능하도록 GitHub에서 Token을 생성합니다.
',9),yn={href:"http://github.com",target:"_blank",rel:"noopener noreferrer"},_n=a('우측 상단의 드롭박스에서 Settings
선택 후 좌측 메뉴 맨 아래의 Developer settings
를 선택합니다.
Developer settings
화면에서 좌측 메뉴 하단 Personal access tockes
를 클릭하고, 화면이 해당 페이지로 변경되면 Generate new token
버튼을 클릭합니다.
Token description에 Token설명을 입력하고 입니다. (e.g. jenkins-integration) 생성합니다. 생성시 repo
, admin:repo_hook
, notifications
항목은 활성화 합니다.
Generate token
버튼을 클릭하여 Token 생성이 완료되면 발급된 Token을 확인 할 수 있습니다. 해당 값은 Jenkins에서 Git연동설정 시 필요합니다.
ADD
트롭박스를 선택합니다. Secret text
로 선택합니다. ADD
버튼 클릭하여 새로운 Credendial을 추가합니다.시스템 설정
화면으로 나오면 Credentials의 -none-
드롭박스에 추가한 Credential을 선택합니다.TEST CONNECTION
버튼을 클릭하여 정상적으로 연결이 되는지 확인합니다. Credentials verified for user Great-Stone, rate limit: 4998
와같은 메시지가 출력됩니다.우측 상단의 드롭박스에서 Settings
선택 후 좌측 메뉴 맨 아래의 Developer settings
를 선택합니다.
Developer settings
화면에서 좌측 메뉴 하단 Personal access tockes
를 클릭하고, 화면이 해당 페이지로 변경되면 Generate new token
버튼을 클릭합니다.
Token description에 Token설명을 입력하고 입니다. (e.g. jenkins-webhook) 생성합니다. 생성시 repo
, admin:repo_hook
, notifications
항목은 활성화 합니다.
Generate token
버튼을 클릭하여 Token 생성이 완료되면 발급된 Token을 확인 할 수 있습니다. 해당 값은 Jenkins에서 Git연동설정 시 필요합니다.
Webhook을 위한 Pipeline
타입의 Item을 추가로 생성합니다. (e.g. 03-04.WebhookBuild Triggering)
설정은 다음과 같이 수행합니다.
Pipeline
설정의 Definition
의 드롭다운을 선택하여 Pipeline script from SCM
을 선택합니다.
SCM
항목은 Git
을 선택하고 하위 필드를 다음과 같이 정의합니다.
Repositories :
Repository URL
을 입력하는데, GitHub에서 git url을 얻기위해서는 웹브라우저에서 해당 repository로 이동하여 Clone or download
버튼을 클릭하여 Url을 복사하여 붙여넣습니다.
Credentials : ADD
트롭박스를 선택합니다.
Username with password
로 선택합니다. ADD
버튼 클릭하여 새로운 Credendial을 추가합니다.시스템 설정
화면으로 나오면 Credentials의 -none-
드롭박스에 추가한 Credential을 선택합니다.Script Path : Pipeline 스크립트가 작성된 파일 패스를 지정합니다. 예제 소스에서는 root 위치에 Jenkinsfile
로 생성되어있으므로 해당 칸에는 Jenkinsfile
이라고 입력 합니다.
저장 후 좌측 메뉴의 Build Now
를 클릭하면 SCM에서 소스를 받고 Pipeline을 지정한 스크립트로 수행하는 것을 확인 할 수 있습니다.
빌드를 수행하기 위한 Worker로 다중 Jenkins를 컨트롤 할 수 있습니다. 이때 명령을 수행하는 Jenkins는 Master
, 빌드를 수행하는 Jenkins는 Worker
로 구분합니다. 여기서는 Worker의 연결을 원격 호스트의 Jenkins를 SSH를 통해 연결하는 방식과 컨테이너로 구성된 Jenkins를 연결하는 과정을 확인 합니다.
Master-Slave 방식, 또는 Master-Agent 방식으로 표현합니다.
※ Slave 호스트에 Jenkins를 설치할 필요는 없습니다.
Step 1. Adding an SSH build agent to Jenkins
Worker가 실행되는 Slave 호스트에 SSH key를 생성하고 Worker 호스트에 인증 키를 복사하는 과정은 다음과 같습니다.
키 생성 및 복사(jenkins 를 수행할 유저를 생성해야 합니다.)
# User가 없는 경우 새로운 Jenkins slave 유저 추가
+$ useradd jenkins
+$ passwd jenkins
+Changing password for user jenkins.
+New password:
+Retype new password:
+
+# Slave 호스트에서 ssh 키를 생성합니다.
+$ ssh-keygen -t rsa
+Generating public/private rsa key pair.
+Enter file in which to save the key (/root/.ssh/id_rsa): <enter>
+Created directory '/root/.ssh'.
+Enter passphrase (empty for no passphrase): <enter>
+Enter same passphrase again: <enter>
+Your identification has been saved in /root/.ssh/id_rsa.
+Your public key has been saved in /root/.ssh/id_rsa.pub.
+The key fingerprint is: <enter>
+SHA256:WFU7MRVViaU1mSmCA5K+5yHfx7X+aV3U6/QtMSUoxug root@jenkinsecho.gyulee.com
+The key's randomart image is:
++---[RSA 2048]----+
+| .... o.+.=*O|
+| .. + . *o=.|
+| . .o. +o. .|
+| . o. + ... +|
+| o.S. . +.|
+| o oE .oo.|
+| = o . . +o=|
+| o . o ..o=|
+| . ..o+ |
++----[SHA256]-----+
+
+$ cd ~/.ssh
+$ cat ./id_rsa.pub > ./authorized_keys
+
Jenkins 관리
의 노드 관리
를 선택합니다.
좌측 메뉴에서 신규 노드
를 클릭합니다.
노드명에 고유한 이름을 입력하고 Permanent Agent
를 활성화 합니다.
새로운 노드에 대한 정보를 기입합니다.
Use this node as much as possible
Launch agent agents via SSH
로 설정합니다. ADD > Jenkins
를 클릭합니다.SSH Username with private key
를 선택합니다.~/.ssh/id_rsa
의 내용을 붙여넣어줍니다. (일반적으로 -----BEGIN RSA PRIVATE KEY-----
로 시작하는 내용입니다.)Non verifying verification strategy
를 선택합니다.빌드 실행 상태
에 새로운 Slave Node가 추가됨을 확인 할 수 있습니다.Label 지정한 Slave Worker에서 빌드가 수행되도록 기존 02-02.Jobs의 Pipeline 스크립트를 수정합니다. 기존 agent any
를 다음과 같이 agent { label 'Metal' }
로 변경합니다. 해당 pipeline은 label이 Metal
로 지정된 Worker에서만 빌드를 수행합니다.
pipeline {
+ agent { label 'Metal' }
+ parameters {
+ string(name: 'Greeting', defaultValue: 'Hello', description: 'How should I greet the world?')
+ }
+ stages {
+ stage('Example') {
+ steps {
+ echo "\${params.Greeting} World!"
+ }
+ }
+ }
+}
+
Step 2. Using Docker images for agents
Master Jenkins 호스트에서 docker 서비스에 설정을 추가합니다. docker 설치가 되어있지 않은 경우 설치가 필요합니다.
$ yum -y install docker
+
RHEL8 환경이 Master인 경우 위와 같은 방식으로 설치를 진행하면 변경된 패키지에 따라
podman-docker
가 설치 됩니다. 아직 Jenkins에서는 2019년 7월 29일 기준podman
을 지원하지 않음으로 별도 yum repository를 추가하여 진행합니다.docker-ce
최신 버전에서는containerd.io
의 필요 버전이1.2.2-3
이상이나 RHEL8에서 지원하지 않음으로 별도로 버전을 지정하여 설치합니다.$ yum -y install yum-utils +$ yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo +$ sudo yum repolist -v +... +Repo-id : docker-ce-stable +Repo-name : Docker CE Stable - x86_64 +Repo-revision: 1564098258 +Repo-updated : Fri 26 Jul 2019 08:44:18 AM KST +Repo-pkgs : 47 +Repo-size : 982 M +Repo-baseurl : https://download.docker.com/linux/centos/7/x86_64/stable +Repo-expire : 172,800 second(s) (last: Thu 25 Jul 2019 07:33:33 AM KST) +Repo-filename: /etc/yum.repos.d/docker-ce.repo +... + +$ yum -y install docker-ce-3:18.09.1-3.el7 +$ systemctl enable docker +$ systemctl start docker +
docker를 설치 한 뒤 API를 위한 TCP 포트를 활성화하는 작업을 진행합니다./lib/systemd/system/docker.service
에 ExecStart
옵션 뒤에 다음과 같이 -H tcp://0.0.0.0:4243
을 추가합니다.
...
+[Service]
+Type=notify
+# the default is not to use systemd for cgroups because the delegate issues still
+# exists and systemd currently does not support the cgroup feature set required
+# for containers run by docker
+ExecStart=/usr/bin/dockerd -H fd:// -H tcp://0.0.0.0:4243
+ExecReload=/bin/kill -s HUP $MAINPID
+TimeoutSec=0
+RestartSec=2
+Restart=always
+...
+
수정 후 서비스를 재시작합니다.
$ systemctl daemon-reload
+$ systemctl restart docker
+$ docker ps
+CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
+
+$ usermod -aG docker jenkins
+$ chmod 777 /var/run/docker.sock
+
Jenkins에 새로운 플러그인을 추가하고 설정합니다.
Jenkins 관리
로 이동하여 플러그인 관리
를 클릭합니다.
설치 가능
탭을 클릭하고 상단의 검색에 docker
를 입력하면 docker
플러그인이 나타납니다. 선택하여 설치를 진행하고 Jenkins를 재시작 합니다.
Jenkins 관리
로 이동하여 시스템 설정
을 클릭합니다.
Cloud
항목 아래 ADD A NEW CLOUD
드롭박스에 있는 Docker
를 선택합니다.
Name은 기본 값으로 진행하고 DOCKER CLOUD DETAILS...
버튼을 클릭합니다.
Docker Host URI : 앞서 설정한 port로 연결합니다. (e.g. tcp://master:4243)
TEST CONNECTION
버튼을 눌러 정상적으로 Version 정보와 API Version이 표기되는지 확인합니다.
Version = 18.09.1, API Version = 1.39
+
Enabled를 활성화 합니다.
Docker 실행을 위한 Item을 생성합니다. (e.g. 04-02.UsingDockerImagesForAgents)
Pipeline
스크립트를 구성합니다.pipeline {
+ agent {
+ docker { image 'node:latest' }
+ }
+ stages {
+ stage('Test'){
+ steps {
+ sh 'node --version'
+ }
+ }
+ }
+}
+
수정 후 좌측 Build Now
를 클릭하여 빌드 수행 후 결과를 확인합니다.
Step 1
에서의 결과와는 달리 Stage View
항목과 Pipeline stage가 수행된 결과를 확인할 수 있는 UI가 생성됩니다.
Step3. Configuring specific agents
Freestyle project
형태의 Item을 생성합니다. (e.g. 04-03.ConfiguringSpecificAgents)
Jenkins는 각 단계, 빌드, 그리고 빌드 후 작업일 지정할 수 있습니다.
Freestyle project
에서는 이같은 전체 빌드 단계를 구성하고 여러가지 플러그인을 사용할 수 있는 환경을 제공합니다.
General
Restrict where this project can be run : 빌드 수행을 특정 Label 노드에 제한하도록 설정할 수 있습니다.
Label Expression : 앞서의 과정에서 생성한 노드 Metal
을 지정해봅니다. 해당 조건의 노드가 존재하는 경우 노드 개수 정보가 표기됩니다.
Label Metal is serviced by 1 node.
+
Build
ADD BUILD STEP
드롭박스에서 Excute shell
항목을 선택하여 추가 합니다. echo "Hello world."
를 넣어봅니다.ADD BUILD STEP
드롭박스에서 Excute shell
항목을 선택하여 추가 합니다. ls -al"
를 넣어봅니다.저장하고 좌측의 Build Now
를 클릭하여 빌드를 수행합니다.
콘솔 출력을 확인하면 지정한 Label 노드에서 각 빌드 절차가 수행된 것을 확인할 수 있습니다.
Jenkins가 유용한 툴인 이유중 하나는 방대한 양의 플러그인 입니다. Jenkins의 기능을 확장시키고, 관리, 빌드 정책 등을 확장 시켜주고, 타 서비스와의 연계를 쉽게 가능하도록 합니다.
',33),Bn={href:"https://plugins.jenkins.io/",target:"_blank",rel:"noopener noreferrer"},Mn=a('Step 1. Adding plugins via plugin manager
Jenkins는 온라인에 연결된 plugin을 검색, 설치할 수 있는 플러그인 관리
기능을 갖고 있습니다. 좌측 메뉴에서 Jenkins 관리
를 클릭하면 플러그인 관리
링크를 통하여 해당 기능에 접근할 수 있습니다.
.hpi
확장자를 갖는 플러그인을 설치하거나 업데이트 사이트를 지정할 수 있습니다.각 플러그인 이름을 클릭하면 플러그인 정보를 확인할 수 있는 plugins.jenkins.io
사이트로 이동하여 정보를 보여줍니다. 사용방법은 우측에 wiki
링크를 클릭합니다. 대략적인 UI나 사용방법은 wiki.jenkins.io
에서 제공합니다.
Step 2. Using shared libraries
',6),Un={href:"https://jenkins.io/doc/book/pipeline/shared-libraries/",target:"_blank",rel:"noopener noreferrer"},Gn={href:"https://github.com/Great-Stone/evenOdd",target:"_blank",rel:"noopener noreferrer"},Hn=a("소스의 var
디렉토리에는 Pipeline에서 사용하는 Shared Library들이 들어있습니다. groovy 스크립트로 되어있으며 Pipeline을 구성한 jenkinsfile
에서 이를 사용합니다.
vars/evenOdd.groovy
를 호출하고 값을 받아오는 형태를 갖고, evenOdd.groovy에서 사용하는 log.info
와 log.warning
은 vars/log.groovy
에 구현되어있습니다.
다음과 같이 Jenkins에 설정을 수행합니다.
Jenkins 관리
클릭 후 시스템 설정
을 선택합니다.Global Pipeline Libraries
의 추가 버튼을 클릭하여 새로운 구성을 추가합니다. Source Code Management
항목이 추가됩니다.GitHub
를 클릭하여 내용을 채웁니다. https://github.com/Great-Stone/evenOdd
인 경우 Great-Stone
이 Owner가 됩니다.Library
에 있는 Load implicitly
를 활성화 합니다.Shared Libraries가 준비가 되면 Pipeline
타입의 Item을 생성하고 (e.g. 05-02.UsingSharedLibraries) Pipeline 설정을 추가합니다.
저장 후 Build Now
를 클릭하여 빌드를 수행합니다. 빌드의 결과로는 2 단계로 수행되는데 1단계는 Declarative: Checkout SCM
으로 SCM으로부터 소스를 받아 준비하는 단계이고, 2단계는 jenkinsfile
을 수행하는 단계입니다. vars/evenOdd.goovy
스크립트에는 stage가 두개 있으나 해당 Pipeline 을 호출하는 값에 따라 하나의 stage만을 수행하도록 되어있어서 하나의 stage가 수행되었습니다.
// Jenkinsfile
+//@Library('evenOdd') _
+
+evenOdd(currentBuild.getNumber())
+
currentBuild.getNumber()
는 현재 생성된 Pipeline Item의 빌드 숫자에 따라 값을 evenOdd(빌드 숫자)
형태로 호출하게 됩니다.
Jenkins shared libraries를 사용하는 가장 좋은 예는 재사용성 있는 Groovy 함수를 타 작업자와 공유하는 것 입니다. 빌드의 상태는 다른 파이프 라인 단계로 계속할 것인지 결정하는 데 사용할 수도 있습니다.
주의
해당 설정은 모든 빌드에 영향을 주기 때문에 타 작업을 위해 추가된 Global Pipeline Libraries의 Library를 삭제하여 진행합니다.
Jenkins빌드의 결과를 받아볼 수 있는 몇가지 방안에 대해 알아봅니다.
Step 1. Notifications of build state
`,9),zn={href:"http://catlight.io",target:"_blank",rel:"noopener noreferrer"},Wn=n("figure",null,[n("img",{src:T,alt:"1564463655933",tabindex:"0",loading:"lazy"}),n("figcaption",null,"1564463655933")],-1),Vn=n("p",null,"여기서는 Chrome 확장 프로그램을 통한 알림을 받을 수 있는 설정을 설명합니다. 이 과정을 진행하기 위해서는 Chrome 웹브라우저가 필요합니다.",-1),Fn=a('jenkins를 검색하여 Yet Another Jenkins Notifier
를 확인합니다. Chrome에 추가
버튼으로 확장 프로그램을 설치합니다.
설치가 완료되면 브라우저 우측 상단에 Jenkins 아이콘이 나타납니다. 클릭합니다.
각 Item(Job)의 url을 입력하여 +
버튼을 클릭합니다.
등록된 Item을 확인하고 해당 빌드를 Jenkins 콘솔에서 실행해봅니다. 결과에 대한 알림이 발생하는 것을 확인 할 수 있습니다.
Step 2. Build state badges for SCM
Jenkins에서 빌드가 수행된 결과를 SCM에 반영하는 기능도 플러그인을 통해 가능합니다. SCM에서 해당 Jenkins에 접근이 가능해야 하므로 Jenkins는 SCM에서 접근가능한 네트워크 상태여야 합니다.
Jenkins에 새로운 플러그인을 추가하고 설정합니다.
Jenkins 관리
로 이동하여 플러그인 관리
를 클릭합니다.설치 가능
탭을 클릭하고 상단의 검색에 embed
를 입력하면 Embeddable Build Status
플러그인이 나타납니다. 선택하여 설치를 진행합니다.Jenkins 관리
로 이동하여 Configure Global Security
을 클릭합니다.Authorization
항목에서 Matrix-based security
를 체크합니다.Authenticated Users
의 경우 필요한 각 항목에 대해 체크박스를 활성화 합니다.Anonymous Users
에 대해서 Job/ViewStatus
항목을 활성화 합니다.이제 기존의 외부 SCM이 연결된 Item을 선택합니다. 여기서는 05-02.UsingSharedLibraries
에 설정합니다. 해당 Item을 선택하면 좌측에 Embeddable Build Status
항목이 새로 생긴것을 확인 할 수 있습니다.
해당 항목을 클릭하고 Markdown
의 unprotected
의 항목을 복사합니다.
[![Build Status](http://myjenkins.com/buildStatus/icon?job=05-02.UsingSharedLibraries)](http://myjenkins.com/job/05-02.UsingSharedLibraries/)
+
# evenOdd
+[![Build Status](http://myjenkins.com/buildStatus/icon?job=libraries)](http://myjenkins.com/job/libraries/)
+
+A Jenkins even/odd playbook from the Jenkins.io documentation
+
+Add this as a shared library called evenOdd in your jenkins
+instance, and then instantiate the pipeline in your project Jenkinsfile
+
+This will also use an example of global variabls from the log.groovy
+definitions
+
+
이같이 반영하면 각 빌드에 대한 결과를 SCM에 동적으로 상태를 반영 할 수 있습니다.
이같은 알림 설정은 코드의 빌드가 얼마나 잘 수행되는지 이해하고 추적할 수 있도록 도와줍니다.
Step 1. Code coverage tests and reports
테스트 Pipeline 구성시 테스트 과정을 지정할 수 있습니다. Testing을 위한 Pipeline
타입의 Item을 추가로 생성합니다. (e.g. 07-01.CodeCoverageTestsAndReports)
설정은 다음과 같이 수행합니다.
Pipeline
스크립트에 다음과 같이 입력 합니다. 테스트와 빌드, 검증 후 결과를 보관하는 단계까지 이루어 집니다.
pipeline {
+ agent any
+ stages {
+ stage('Build') {
+ steps {
+ sh '''
+ echo This > app.sh
+ echo That >> app.sh
+ '''
+ }
+ }
+ stage('Test') {
+ steps {
+ sh '''
+ grep This app.sh >> \${BUILD_ID}.cov
+ grep That app.sh >> \${BUILD_ID}.cov
+ '''
+ }
+ }
+ stage('Coverage'){
+ steps {
+ sh '''
+ app_lines=\`cat app.sh | wc -l\`
+ cov_lines=\`cat \${BUILD_ID}.cov | wc -l\`
+ echo The app has \`expr $app_lines - $cov_lines\` lines uncovered > \${BUILD_ID}.rpt
+ cat \${BUILD_ID}.rpt
+ '''
+ archiveArtifacts "\${env.BUILD_ID}.rpt"
+ }
+ }
+ }
+}
+
빌드가 완료되면 해당 Job화면을 리로드 합니다. Pipeline에 archiveArtifacts
가 추가되었으므로 해당 Job에서 이를 관리합니다.
해당 아카이브에는 코드 검증 후의 결과가 저장 됩니다.
Step 2. Using test results to stop the build
테스트 결과에 따라 빌드를 중지시키는 Pipeline 스크립트를 확인합니다. Testing을 위한 Pipeline
타입의 Item을 추가로 생성합니다. (e.g. 07-02.UsingTestResultsToStopTheBuild)
설정은 다음과 같이 수행합니다.
Pipeline
스크립트에 다음과 같이 입력 합니다. 테스트와 빌드, 검증 후 결과를 보관하는 단계까지 이루어 집니다.
pipeline {
+ agent any
+ stages {
+ stage('Build') {
+ steps {
+ sh '''
+ echo This > app.sh
+ echo That >> app.sh
+ echo The Other >> app.sh
+ '''
+ }
+ }
+ stage('Test') {
+ steps {
+ sh '''
+ for n in This That Those
+ do if grep $n app.sh >> \${BUILD_ID}.cov
+ then exit 1
+ fi
+ done
+ '''
+ }
+ }
+ stage('Coverage'){
+ steps {
+ sh '''
+ app_lines=\`cat app.sh | wc -l\`
+ cov_lines=\`cat \${BUILD_ID}.cov | wc -l\`
+ echo The app has \`expr $app_lines - $cov_lines\` lines uncovered > \${BUILD_ID}.rpt
+ cat \${BUILD_ID}.rpt
+ '''
+ archiveArtifacts "\${env.BUILD_ID}.rpt"
+ }
+ }
+ }
+}
+
저장을 하고 빌드를 수행하면, Pipeline 스크립트 상 Test
Stage에서 조건 만족 시 exit 1
를 수행하므로 빌드는 중간에 멈추게 됩니다.
Jenkins는 외부 서비스와의 연동이나 정보 조회를 위한 API를 제공합니다.
Step 1. Triggering builds via the REST API
Jenkins REST API 테스트를 위해서는 Jenkins에 인증 가능한 Token을 취득하고 curl이나 Postman 같은 도구를 사용하여 확인 가능 합니다. 우선 Token을 얻는 방법은 다음과 같습니다.
Jenkins에 로그인 합니다.
우측 상단의 로그인 아이디에 마우스를 호버하면 드롭박스 버튼이 나타납니다. 설정
을 클릭합니다.
API Token
에서 Current token
을 확인합니다. 등록된 Token이 없는 경우 다음과 같이 신규 Token을 발급 받습니다.
ADD NEW TOKEN
을 클릭합니다.
이름을 기입하는 칸에 로그인 한 아이디를 등록합니다. (e.g. admin)
GENERATE
를 클릭하여 Token을 생성합니다.
이름과 Token을 사용하여 다음과 같이 curl로 접속하면 Jenkins-Crumb
프롬프트가 나타납니다.
$ curl --user "admin:TOKEN" 'http://myjenkins.com/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,":",//crumb)'
+
+Jenkins-Crumb:89e1fd9c402824c89465f6b97f49b605
+
Crumb
를 확인했으면 다시 헤더 값에 Jenkins-Crumb:
를 추가하여 02-04.MultiStep
Job을 빌드하기 위해 다음과 같이 요청합니다.
$ curl -X POST http://myjenkins.com/job/02-04.MultiStep/build --user gyulee:11479bdec9cada082d189938a3946348be --data-urlencode json='' -H "Jenkins-Crumb:89e1fd9c402824c89465f6b97f49b605"
+
API로 호출된 빌드가 수행되어 빌드 번호가 증가하는 것을 확인합니다.
Step 2. Retriving build status via the REST API
빌드에 대한 결과를 REST API를 통해 요청하는 방법을 알아봅니다. 앞서 진행시의 Token값이 필요합니다. Json 형태로 출력되기 때문에 정렬을 위해 python이 설치 되어있다면 mjson.tool
을 사용하여 보기 좋은 형태로 출력 가능합니다.
# Python이 설치되어있지 않은 경우
+$ yum -y install python2
+
+# Jenkins에 REST API로 마지막 빌드 상태 요청
+$ curl -s --user gyulee:11479bdec9cada082d189938a3946348be http://myjenkins.com/job/02-04.MultiStep/lastBuild/api/json | python2 -mjson.tool
+
+{
+ "_class": "org.jenkinsci.plugins.workflow.job.WorkflowRun",
+ "actions": [
+ {
+ "_class": "hudson.model.CauseAction",
+ "causes": [
+ {
+ "_class": "hudson.model.Cause$UserIdCause",
+ "shortDescription": "Started by user GyuSeok.Lee",
+ "userId": "gyulee",
+ "userName": "GyuSeok.Lee"
+ }
+ ]
+ },
+ {},
+ {
+ "_class": "hudson.plugins.git.util.BuildData",
+ "buildsByBranchName": {
+ "master": {
+ "_class": "hudson.plugins.git.util.Build",
+ "buildNumber": 5,
+ "buildResult": null,
+...
+
Step 1. Securing your deployment with users
사용자별 배포수행을 위한 사용자 설정을 설명합니다.
Jenkins 관리
로 이동하여 Configure Global Security
를 클릭합니다.Enable security
는 보안 설정 여부를 설정하는 항목으로 기본적으로는 비활성화되어있습니다. 체크하여 활성화하면 다양한 보안 옵션을 설정할 수 있는 항목이 표기 됩니다.
Security Realm 에서는 Jenkins에서 사용하는 사용자 관리 방식을 선택합니다.
사용자의 가입 허용
이 활성화되면 Jenkins 에 접속하는 사용자는 스스로 계정을 생성하고 접근 가능합니다.Authorization 에서는 사용자 권한에 대한 설정을 정의합니다.
1.164
이전 버전의 동작과 동일하게 관리됩니다. Admin
사용자만 모든 기능을 수행하며, 일반 사용자와 비로그인 사용자는 읽기만 가능합니다.다음은 권한 매트릭스의 항목과 권한별 설명입니다.
항목 | 권한 | 의미 |
---|---|---|
Overall | Administer | 시스템의 전역 설정을 변경할 수 있다. OS 에서 허용된 범위안에서 전체 시스템 엑세스드의 매우 민감한 설정을 수행 |
Read | 젠킨스의 모든 페이지 확인 가능 | |
RunScripts | 그루비 콘솔이나 그루비 CLI 명령을 통해 그루비 스크립트를 실행 | |
UploadPlugins | 특정 플러그인을 업로드 | |
ConfigureUpdateCenter | 업데이트 사이트와 프록시 설정 | |
Slave | Configure | 기존 슬레이브 설정 가능 |
Delete | 기존 슬레이브 삭제 | |
Create | 신규 슬레이브 생성 | |
Disconnect | 슬레이브 연결을 끊거나 슬레이브를 임시로 오프라인으로 표시 | |
Connect | 슬레이브와 연결하거나 슬레이브를 온라인으로 표시 | |
Job | Create | 새로운 작업 생성 |
Delete | 기존 작업 삭제 | |
Configure | 기존 작업의 설정 갱신 | |
Read | 프로젝트 설정에 읽기 전용 권한 부여 | |
Discover | 익명 사용자가 작업을 볼 권한이 없으면 에러 메시지 표시를 하지 않고 로그인 폼으로 전환 | |
Build | 새로운 빌드 시작 | |
Workspace | 젠킨스 빌드를 실행 하기 위해 체크아웃 한 작업 영역의 내용을 가져오기 가능 | |
Cancel | 실행중인 빌드 취소 | |
Run | Delete | 빌드 내역에서 특정 빌드 삭제 |
Update | 빌드의 설명과 기타 프로퍼티 수정(빌드 실패 사유등) | |
View | Create | 새로운 뷰 생성 |
Delete | 기존 뷰 삭제 | |
Configure | 기존 뷰 설정 갱신 | |
Read | 기존 뷰 보기 | |
SCM | Tag | 특정 빌드와 관련된 소스 관리 시스템에 태깅을 생성 |
CSRF Protection 항목에 있는 Prevent Cross Site Request Forgery exploits
항목은 페이지마다 nonce 또는 crumb 이라 불리우는 임시 값을 삽입하여 사이트 간 요청 위조 공격을 막을 수 있게 해줍니다. 사용방법은 위에서 REST API 에 대한 설명 시 crumb 값을 얻고, 사용하는 방법을 참고합니다.
Step 2. Securing secret credentials and files
Jenkins에서 Pipeline을 설정하는 경우 일부 보안적인 값이 필요한 경우가 있습니다. 예를 들면 Username
과 Password
같은 값입니다. 앞서의 과정에서 Credentials
를 생성하는 작업을 일부 수행해 보았습니다. 여기서는 생성된 인증 값을 Pipeline에 적용하는 방법을 설명합니다.
Pipeline
타입의 Item을 추가로 생성합니다. (e.g. 09-02.SecuringSecretCredentialsAndFiles) 설정은 다음과 같이 수행합니다.
Pipeline
스크립트에 다음과 같이 입력 합니다.
pipeline {
+ agent any
+ environment {
+ SECRET=credentials('jenkins-secret-text')
+ }
+ stages {
+ stage('Build') {
+ steps {
+ echo "\${env.SECRET}"
+ }
+ }
+ }
+}
+
저장 후 Build Now
를 클릭하여 빌드를 수행하면 실패하게 되고 Console Output
에서 진행사항을 보면, Pipeline 스크립트에서 선언한 jenkins-secret-text
때문에 에러가 발생한 것을 확인할 수 있습니다.
좌측 상단의 Jenkins
버튼을 클릭하여 최상위 메뉴로 이동합니다.
좌측 메뉴의 Credentials
를 클릭하고 (global)
도메인을 클릭합니다.
좌측에 Add Credentials
를 클릭하여 새로운 항목을 추가합니다.
저장 후 다시 빌드를 수행하면 정상적으로 수행됩니다. 해당 값은 숨기기 위한 값이므로 Pipeline 스크립트에서 echo
로 호출하더라도 ****
이란 값으로 표기 됩니다.
이같은 방법은 Password같은 보안에 민감한 정보를 사용하기에 유용합니다.
Step 3. Auditing your environment
Jenkins의 변화와 활동에 대한 감시를 위한 설정 방법을 설명합니다. Jenkins에 새로운 플러그인을 추가하고 설정합니다.
Jenkins 관리
로 이동하여 플러그인 관리
를 클릭합니다.설치 가능
탭을 클릭하고 상단의 검색에 audit
를 입력하면 Audit Trail
플러그인이 나타납니다. 선택하여 설치합니다.Jenkins 관리
로 이동하여 시스템 설정
을 클릭합니다.ADD LOGGER
드롭박스에서 Log File
을 선택하여 설정합니다. 저장 후 빌드나 Job의 설정 변경등의 작업을 수행하면, audit.log.0
으로 지정된 파일 경로에 생성됨을 확인 할 수 있습니다.
$ tail -f ./audit.log.0
+Jul 31, 2019 10:47:32,727 AM job/02-02.Jobs/ #12 Started by user GyuSeok.Lee
+Jul 31, 2019 10:47:42,738 AM /job/03-04.WebhookBuild Triggering/configSubmit by gyulee
+Jul 31, 2019 10:48:09,001 AM /configSubmit by gyulee
+
Step 4. Using forders to create security realms
다양한 프로젝트를 관리하는 경우 관리상, 빌드 프로젝트를 관리해야할 필요성이 발생합니다. Jenkins에서 Forder 아이템을 생성하여 관리 편의성과 보안요소를 추가할 수 있습니다.
우선 테스트를 위한 사용자를 추가합니다.
Jenkins 관리
를 클릭하여 Manage Users
로 이동합니다.사용자 생성
을 클릭하여 새로운 사용자를 추가합니다. 다음으로 Forder 타임의 Item을 추가합니다.
새로운 Item
을 클릭하여 이름을 02-Project
로 예를 들어 지정하고, Forder를 클릭하여 OK
버튼을 클릭합니다.SAVE
버튼을 클릭하고 좌측 상단의 Jenkins
버튼을 클릭하여 최상위 페이지로 이동합니다.02-02.Jobs
에 마우스를 대면 드롭박스 메뉴를 확장할 수 있습니다. Move
를 클릭합니다.Jenkins >> 02-Project
를 선택하고 MOVE
버튼을 클릭합니다. 다시 최상위 메뉴로 오면 02-02.Jobs
가 사라진 것을 확인할 수 있습니다. 02
로 시작하는 다은 프로젝트도 같은 작업을 수행하여 이동시킵니다.02-Project
를 클릭하면 이동된 프로젝트들이 나타납니다.권한 설정을 하여 현재 Admin 권한의 사용자는 접근 가능하고 새로 생성한 tester는 접근불가하도록 설정합니다.
Folder에 접근하는 권한을 설정하기위해 Jenkins 관리
의 Configure Global Security
로 이동합니다.
Authorization항목의 Project-based Matrix Authorization Strategy
를 선택합니다.
ADD USER OR GROUP...
을 클릭하여 Admin 권한의 사용자를 추가합니다.
Admin 권한의 사용자에게는 모든 권한을 주고 Authenticated Users
에는 Overall의 Read
권한만 부여합니다.
생성한 02-Project
로 이동하여 좌측 메뉴의 Configure
를 클릭합니다.
Properties에 추가된 Enable project-based security
를 확성화하면 항목별 권한 관리 메트릭스가 표시됩니다. Job의 Build, Read, ViewStatus, Workspace를 클릭하고 View의 Read를 클릭하여 권한을 부여합니다.
로그아웃 후에 앞서 추가한 test
사용자로 로그인 하면 기본적으로 다른 프로젝트나 Item들은 권한이 없기 때문에 보이지 않고, 앞서 설정한 02-Project
폴더만 리스트에 나타납니다.
Jenkins의 인증 기능을 사용하여 보안적 요소를 구성할 수 있습니다. Audit 로그를 활용하여 사용자별 활동을 기록할 수도 있고 Folder를 활용하면 간단히 사용자/그룹에 프로젝트를 구분하여 사용할 수 있도록 구성할 수 있습니다.
빌드 이후 빌드의 결과를 기록하고 저장하는 방법을 설명합니다.
Step 1. Creating and storing artifacts
Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 10-01.CreatingAndStoringArtifacts)
Pipeline에 다음과 같이 스크립트를 추가합니다.
pipeline {
+ agent any
+ stages{
+ stage('Build') {
+ steps{
+ sh 'echo "Generating artifacts for \${BUILD_NUMBER}" > output.txt'
+ }
+ }
+ stage('Archive') {
+ steps {
+ archiveArtifacts artifacts: 'output.txt', onlyIfSuccessful: true
+ }
+ }
+ }
+}
+
Archive
Stage에 archiveArtifacts
스크립트가 동작하는 예제입니다. 이같은 Pipeline 스크립트 작성을 도와주는 툴을 추가로 확인해 봅니다.
Pipeline Syntax
링크를 클릭합니다.Sample Step
에서 archiveArtifacts: Archive the artifacts
를 선택합니다. 고급...
을 클릭합니다.GENERATE PIPELINE SCRIPT
를 클릭합니다.결과물을 확인하면 Pipeline 스크립트에 작성한 형태와 같은 것을 확인 할 수 있습니다.
좌측 메뉴의 Build Now
를 클릭하여 빌드 수행 후에 화면에 Artifacts 항목이 추가된 것을 확인할 수 있습니다. UI 상에는 마지막 빌드 결과가 강조되어 나오고 각 빌드에 대한 결과물은 각각의 빌드단계의 다운로드 버튼으로 확인하고 다운로드 할 수 있습니다.
Step 2. Fingerprinting for artifact tracking
빌드 이후 보관되는 파일에 대해 어떤 프로젝트, 어떤 빌드 에서 발생한 결과물인지 확인할 수 있는 핑거프린팅 기능을 설명합니다.
Step 1
의 프로젝트를 그대로 사용하거나 Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 10-02.FingerprintingForArtifactTracking)
Step 1
Pipeline 스크립트의 archiveArtifacts
에 fingerprint: true
를 추가합니다.
pipeline {
+ agent any
+ stages{
+ stage('Build') {
+ steps{
+ sh 'echo "Generating text artifacts: Build:\${BUILD_NUMBER}" > output.txt'
+ }
+ }
+ stage('Archive') {
+ steps {
+ archiveArtifacts artifacts: 'output.txt', fingerprint: true, onlyIfSuccessful: true
+ }
+ }
+ }
+}
+
파일의 지문을 확인합니다.
첫번째 빌드를 수행하고 빌드 결과 아카이브 파일 output.txt
파일을 다운로드 받습니다. (파일을 우클릭하고 다른 이름으로 링크 저장...
or Download Linked File
을 클릭하여 파일을 받습니다.)
좌측 상단의 Jenkins
를 클릭하여 최상위 메뉴로 돌아갑니다.
좌측 메뉴의 파일 핑거프린트 확인
을 클릭합니다.
파일 선택
버튼을 클릭하여 앞서 다운로드한 파일을 선택하고 확인하기
버튼을 클릭합니다.
어떤 프로젝트의 몇번째 빌드에서 발생한 파일인지 확인합니다.
두번째 빌드를 수행하고 파일 핑거프린트를 확인해 봅니다.
빌드 번호 정보가 변경된 것을 확인합니다.
Pipeline에 대해 설명합니다.
Step 1. Automating deployment with pipelines
Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 11-01.AutomatingDeploymentWithPipelines)
Pipeline에 다음과 같은 스크립트를 입력합니다.
pipeline {
+ agent any
+ stages {
+ stage('Build') {
+ steps {
+ sh 'echo "Hello World"'
+ }
+ }
+ stage('Test') {
+ steps {
+ sh 'echo "Test Hello World!"'
+ }
+ }
+ }
+}
+
두개의 Stage를 갖는 Pipeline 스크립트입니다. Pipeline은 빌드 수행시의 각 단계를 구분하여 빌드의 과정을 확인하고 실패에 따른 단계별 확인이 가능합니다.
좌측 Build Now
를 클릭하여 빌드를 수행하면 빌드에 대한 결과는 Stage 별로 성공 실패의 여부와 로그를 확인할 수 있도록 Stage View
가 UI로 제공됩니다. Stage 별로 Stage View는 기록되며, Stage에 변경이 있거나 이름이 변경되는 경우에는 해당 UI에 변경이 발생하여 기존 Pipeline 기록을 보지 못할 수 있습니다.
Step 2. Creating pipeline gates
Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 11-02.CreatingPipelineGates)
Pipeline에 다음과 같은 스크립트를 입력합니다.
pipeline {
+ agent any
+ stages {
+ stage('Build') {
+ steps {
+ sh 'echo "Hello World"'
+ }
+ }
+ stage('BuildMore'){
+ steps {
+ input message: "Shall we build more?"
+ sh '''
+ echo "We are approved; continue!"
+ ls -lah
+ '''
+ }
+ }
+ }
+}
+
개의 Stage를 갖는 Pipeline 스크립트입니다. 두번째 Stage에 input
스크립트가 있습니다. 이 스크립트가 추가되면 Pipeline을 진행하면서 해당하는 동작을 수행할 것인지, 마치 승인 작업과 같은 동작을 수행할 수 있습니다.
좌측 Build Now
를 클릭하여 빌드를 수행하면 두번째 Stage에서 해당 작업을 수행할 지에 대한 물음을 확인 할 수 있습니다.
Abort
를 선택하면 빌드 취소와 같은 동작으로 실패로 처리되지는 않습니다.
Step 3. Job promotion for long-running pipeline
빌드 단계를 구현할 때 Pipeline 스크립트로 하나의 프로젝트 내에서 모든 동작을 정의 할 수도 있지만 서로다른 Job을 연계하고, 승인 절차를 따르도록 구성할 수 있습니다.
Job promotion 기능을 사용하기 위한 플러그인을 설치합니다.
Jenkins 관리
에서 플러그인 관리
를 선택합니다.설치 가능
탭을 클릭하고 상단의 검색에 promoted
를 입력하면 promoted builds
를 확인 할 수 있습니다. 설치합니다.FreeStyle 타입의 Item을 생성합니다. (e.g. 11-03.Job-one)
General 탭의 Promote builds when...
를 활성화 하여 설정합니다.
Only when manually approved
활성화 ADD PRAMETER
드롭박스에서 Boolean Parameter
를 선택합니다. Build 드롭박스에서 Execute shell
을 선택합니다.
다음을 입력합니다.
echo 'This is the Job-one'
+
저장하면 생성된 프로젝트에 Promotion Status
항목이 추가되어 생성됩니다.
11-03.Job-one
빌드 후 승인에 대한 다음 빌드를 진행할 FreeStyle 타입의 Item을 생성합니다. (e.g. 11-03.Job-two)
빌드 유발 항목에서 Build when another project is promoted
를 활성화 합니다. 어떤 Job에서 promote 상황이 발생하였을 때 빌드를 수행할지 지정합니다.
Build 드롭박스에서 Execute shell
을 선택합니다.
다음을 입력합니다.
echo 'This is the Job-two'
+
11-03.Job-one
에 대한 빌드를 수행합니다. 수행 완료 후 빌드 히스토리의 최근 빌드를 클릭(e.g. #1)하면 Promotion Status
에 승인절차를 기다리고 있음을 확인할 수 있습니다. Parameters 항목의 approve
를 체크하고 APPROVE
버튼을 클릭합니다.
승인이 완료되면 해당 프로젝트의 승인에 대한 이벤트를 통해 빌드를 수행하는 11-03.Job-two
가 이어서 빌드됨을 확인 할 수 있습니다.
Step 4. Multibranch repository automation
SCM의 Multibranch를 빌드하는 과정에 대해 설명합니다.
다음의 GitHub repository를 fork 합니다.
',104),Xn={href:"https://github.com/Great-Stone/multibranch-demo",target:"_blank",rel:"noopener noreferrer"},Qn=a('Multibranch Pipeline 형태의 Item을 생성합니다. (e.g. 11-04.MultibranchRepositoryAutomation)
ADD SOURCE
드롭박스에서 GitHub를 클릭합니다. VALIDATE
버튼을 클릭하여 잘 접근 되는지 확인합니다.Periodically if not otherwise run
를 활성화 합니다. 1 minute
으로 설정합니다.저장 후에는 자동적으로 모든 브랜치의 소스를 빌드 수행합니다.
SCM에서 브랜치를 여러개 관리하고 모두 빌드와 테스팅이 필요하다면 Multibranch 프로젝트를 생성하여 등록하고, 빌드 관리가 가능합니다.
Step 5. Creating pipeline with snippets
Pipeline 을 스크립트를 작성하는 방법을 배워봅니다. Pipeline 타입의 Item을 생성합니다. (e.g. 11-05. CreatingPipelineWithSnippets)
Pipeline에 다음과 같은 스크립트를 입력합니다.
pipeline {
+ agent any
+ stages {
+ stage("Hello") {
+ steps {
+ echo 'Hello World'
+ }
+ }
+ }
+}
+
echo가 동작할때 시간을 기록하도록 스크립트를 수정해보겠습니다.
Pipeline Syntax 링크를 클릭합니다.
Sample Step에서 timestamps: timestamps
를 선택하고 GENERATE PIPELINE SCRIPT
버튼을 클릭합니다.
timestamps {
+ // some block
+}
+
사용방식을 확인하고 앞서 Pipeline 스크립트의 stage에 시간을 기록하도록 수정합니다.
...
+stage("Hello") {
+ steps {
+ timestamps {
+ echo 'Hello World'
+ }
+ }
+}
+...
+
빌드를 수행하고 로그를 확인해 봅니다. echo 동작이 수행 될때 시간이 함께 표기되는 것을 확인 할 수 있습니다.
Step 6. Discovering global pipeline variables
Pipeline에서 사용할 수 있는 변수를 확인하고 사용하는 방법을 알아봅니다. Pipeline 타입의 Item을 생성합니다. (e.g. 11-06.DiscoveringGlobalPipelineVariables)
Pipeline에 다음과 같은 스크립트를 입력합니다.
pipeline {
+ agent any
+ stages {
+ stage('Build') {
+ steps {
+ echo "We are in build \${currentBuild.number}"
+ echo "Our current result is \${currentBuild.currentResult}"
+ }
+ }
+ stage('BuildMore'){
+ steps {
+ echo "Name of the project is \${currentBuild.projectName}"
+ }
+ }
+ stage('BuildEnv'){
+ steps {
+ echo "Jenkins Home : \${env.JENKINS_HOME}"
+ }
+ }
+ }
+}
+
Pipeline 스크립트에서 사용가능한 변수와 사용방법은 Pipeline Syntax
링크의 Global Variables Reference
항목에서 확인 가능합니다.
GitHub SCM 연동 이슈
GitHub를 SCM으로 사용하는 경우 다음과 같은 메시지가 출력되면서 진행되지 않는 경우가 있습니다.
GitHub API Usage: Current quota has 5000 remaining (447380 over budget). Next quota of 5000 in 5 days 0 hr. Sleeping for 4 days 23 hr.
+14:07:33 GitHub API Usage: The quota may have been refreshed earlier than expected, rechecking...
+
이 경우 서버 시간과 GitHub의 시간이 맞지 않아 발생할 수 있는 이슈 입니다. ntpdate를 재설정 합니다.
`,25),ns=n("li",null,[n("p",null,"RHEL7 : ntpd를 재시작 합니다."),n("div",{class:"language-bash","data-ext":"sh","data-title":"sh"},[n("pre",{class:"language-bash"},[n("code",null,`$ systemctl restart ntpd +`)])])],-1),ss=n("br",null,null,-1),es={href:"https://access.redhat.com/solutions/4130881",target:"_blank",rel:"noopener noreferrer"},as=n("div",{class:"language-bash","data-ext":"sh","data-title":"sh"},[n("pre",{class:"language-bash"},[n("code",null,[s(`$ systemctl stop chronyd +$ chronyd `),n("span",{class:"token parameter variable"},"-q"),s(` +$ systemctl start chronyd +`)])])],-1),is=n("p",null,[n("strong",null,"유용한 플러그인")],-1),ts=n("ul",null,[n("li",null,"Restart Safely : Jenkins를 재기동해야하는 경우 빌드가 수행중이지 않을 때 자동으로 Restart 시켜줍니다. 설치 후에는 왼쪽 주 메뉴에 표시됩니다."),n("li",null,"ThinBackup : Jenkins의 구성을 백업, 복구할 수 있는 기능을 제공합니다. 백업 주기나 백업 개수등을 정의 할 수 있습니다.")],-1);function ls(os,ps){const i=t("ExternalLinkIcon"),l=t("Mermaid"),o=t("RouteLink");return X(),Q("div",null,[en,n("blockquote",null,[n("p",null,[s("참고 url : "),n("a",an,[s("https://pkg.jenkins.io/redhat-stable/"),e(i)])])]),tn,n("ul",null,[n("li",null,[n("p",null,[n("a",ln,[s("http://afonsof.com/jenkins-material-theme/"),e(i)]),s(" 에 접속합니다.")])]),on]),pn,cn,dn,e(l,{id:"mermaid-235",code:"eJxtjcEJwzAQBN9RFWogDeThT1xAIGngkBdzIN8p0sng7mOHECPjfQ6zuwXvCgnomcZMk7skysaBE4n5HjOipha+UKwlT6MRLXpkHWowVnF+zW/o2nVb++bvKsZStRbPYlifT9Tv7N+1TR4QeUZeDub+dtRT1GWCmPsA04VS8A=="}),rn,e(l,{id:"mermaid-292",code:"eJxtjk0KwkAMRveeYpbpolcQpr8qCmLBfabEOlJmJKZYb68NIhaa5XuP8HWM96vZn1bmcxZ20SUmTdcZWPcQxlaOHG/USqJBNjlTwQGF/bigapjBWuEGKiZq5NXTgt1+v+UxXHw3MIqPYdZZ7XJoZHDTwD9YwNnT8wcLhSWUoxAH7FW8AUDhPw0="}),un,n("p",null,[s("Jenkins가 설치된 서버에 [피보나치 수열](["),n("a",vn,[s("https://namu.wiki/w/피보나치 수열"),e(i)]),s("]("),n("a",gn,[s("https://namu.wiki/w/피보나치"),e(i)]),s(" 수열))을 수행하는 스크립트를 작성합니다. Sleep이 있기 때문에 일정 시간 이상 소요 됩니다.")]),kn,n("ul",null,[mn,bn,n("li",null,[s("Repositories "),n("ul",null,[n("li",null,[s("Repository URL : "),n("a",hn,[s("https://github.com/Great-Stone/jenkins-git"),e(i)])])])])]),fn,n("ul",null,[n("li",null,[n("p",null,[n("a",yn,[s("github.com"),e(i)]),s("에 접속하여 로그인합니다.")])]),_n]),Sn,n("ul",null,[xn,Pn,n("li",null,[s("항목의 입력정보는 다음과 같습니다. "),n("ul",null,[Jn,n("li",null,[s("API URL : "),n("a",Cn,[s("https://api.github.com"),e(i)])]),Tn])]),In]),An,wn,n("ul",null,[n("li",null,[n("p",null,[n("a",Dn,[s("https://github.com/Great-Stone/jenkins-git"),e(i)]),s(" 를 "),jn,s("합니다.")]),qn]),En]),Rn,n("p",null,[n("a",Bn,[s("Plugin Index"),e(i)])]),Mn,n("p",null,[s("Jenkins Pipeline의 Shared libraries에 대한 상세 내용은 다음 링크를 참고합니다. "),n("a",Un,[s("https://jenkins.io/doc/book/pipeline/shared-libraries/"),e(i)])]),n("p",null,[s("이번 실습을 진행하기전에 GitHub에서 "),n("a",Gn,[s("https://github.com/Great-Stone/evenOdd"),e(i)]),s(" repository를 본인 계정의 GitHub에 Fork 하여 진행합니다.")]),Hn,n("ul",null,[Ln,Nn,n("li",null,[s("Repositories "),n("ul",null,[n("li",null,[s("Repository URL : "),n("a",On,[s("https://github.com/Great-Stone/evenOdd.git"),e(i)])])])])]),$n,n("p",null,[s("Jenkins에서는 플러그인이나 외부 툴에 의해 빌드에 대한 결과를 받아 볼 수 있습니다. 대표적으로는 Jenkins의 슬랙 플러그인을 사용하여 슬랙으로 빌드에 결과를 받아보거나, "),n("a",zn,[s("catlight.io"),e(i)]),s(" 에서 데스크탑용 어플리케이션에 연동하는 방법도 있습니다.")]),Wn,Vn,n("ol",null,[n("li",null,[n("p",null,[e(o,{to:"/05-Software/Jenkins/pipeline101/chrome:/apps/"},{default:nn(()=>[s("chrome://apps/")]),_:1}),s("에 접속하여 앱스토어를 클릭합니다.")])]),Fn]),Kn,n("p",null,[s("복사한 형식을 GitHub의 evenOdd repository의 "),n("a",Zn,[s("README.md"),e(i)]),s(" 파일 상단에 위치 시킵니다.")]),Yn,n("ul",null,[n("li",null,[n("a",Xn,[s("https://github.com/Great-Stone/multibranch-demo"),e(i)])])]),Qn,n("ul",null,[ns,n("li",null,[n("p",null,[s("RHEL8 : RHEL8에서는 ntpdate를 사용하지 않고 chronyd가 대신합니다."),ss,n("a",es,[s("https://access.redhat.com/solutions/4130881"),e(i)])]),as])]),is,ts])}const ys=Y(sn,[["render",ls],["__file","13-jenkins_101_single.html.vue"]]),_s=JSON.parse('{"path":"/05-Software/Jenkins/pipeline101/13-jenkins_101_single.html","title":"Pipeline on Jenkins 101 (Single Page)","lang":"ko-KR","frontmatter":{"description":"jenkins 101","tag":["cicd","jenkins"],"head":[["meta",{"property":"og:url","content":"https://docmoa.github.io/05-Software/Jenkins/pipeline101/13-jenkins_101_single.html"}],["meta",{"property":"og:site_name","content":"docmoa"}],["meta",{"property":"og:title","content":"Pipeline on Jenkins 101 (Single Page)"}],["meta",{"property":"og:description","content":"jenkins 101"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"http://myjenkins.com/buildStatus/icon?job=05-02.UsingSharedLibraries"}],["meta",{"property":"og:locale","content":"ko-KR"}],["meta",{"property":"og:updated_time","content":"2023-09-18T13:12:54.000Z"}],["meta",{"name":"twitter:card","content":"summary_large_image"}],["meta",{"name":"twitter:image:alt","content":"Pipeline on Jenkins 101 (Single Page)"}],["meta",{"property":"article:tag","content":"cicd"}],["meta",{"property":"article:tag","content":"jenkins"}],["meta",{"property":"article:modified_time","content":"2023-09-18T13:12:54.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Pipeline on Jenkins 101 (Single Page)\\",\\"image\\":[\\"http://myjenkins.com/buildStatus/icon?job=05-02.UsingSharedLibraries\\",\\"http://myjenkins.com/buildStatus/icon?job=libraries\\"],\\"dateModified\\":\\"2023-09-18T13:12:54.000Z\\",\\"author\\":[]}"]]},"headers":[{"level":2,"title":"Introduction","slug":"introduction","link":"#introduction","children":[]},{"level":2,"title":"1. CI/CD","slug":"_1-ci-cd","link":"#_1-ci-cd","children":[]},{"level":2,"title":"2. Jobs","slug":"_2-jobs","link":"#_2-jobs","children":[]},{"level":2,"title":"3. Builds","slug":"_3-builds","link":"#_3-builds","children":[]},{"level":2,"title":"4. Agents and Distributing Builds","slug":"_4-agents-and-distributing-builds","link":"#_4-agents-and-distributing-builds","children":[]},{"level":2,"title":"5. Plugins","slug":"_5-plugins","link":"#_5-plugins","children":[]},{"level":2,"title":"6. Notifications","slug":"_6-notifications","link":"#_6-notifications","children":[]},{"level":2,"title":"7. Testing","slug":"_7-testing","link":"#_7-testing","children":[]},{"level":2,"title":"8. REST API","slug":"_8-rest-api","link":"#_8-rest-api","children":[]},{"level":2,"title":"9. Security","slug":"_9-security","link":"#_9-security","children":[]},{"level":2,"title":"10. Artifacts","slug":"_10-artifacts","link":"#_10-artifacts","children":[]},{"level":2,"title":"11. Pipelines","slug":"_11-pipelines","link":"#_11-pipelines","children":[]},{"level":2,"title":"Apendix","slug":"apendix","link":"#apendix","children":[]}],"git":{"createdTime":1640328154000,"updatedTime":1695042774000,"contributors":[{"name":"Administrator","email":"admin@example.com","commits":1},{"name":"Great-Stone","email":"hahohh@gmail.com","commits":1}]},"readingTime":{"minutes":13.08,"words":3923},"filePathRelative":"05-Software/Jenkins/pipeline101/13-jenkins_101_single.md","localizedDate":"2021년 12월 24일","excerpt":"\\n\\n\\nUpdate at 31 Jul, 2019
\\n
Jenkins Pipeline 을 구성하기 위해 VM 환경에서 Jenkins와 관련 Echo System을 구성합니다. 각 Product의 버전은 문서를 작성하는 시점에서의 최신 버전을 위주로 다운로드 및 설치되었습니다. 구성 기반 환경 및 버전은 필요에 따라 변경 가능합니다.
\\nCategory | \\nName | \\nVersion | \\n
---|---|---|
VM | \\nVirtualBox | \\n6.0.10 | \\n
OS | \\nRed Hat Enterprise Linux | \\n8.0.0 | \\n
JDK | \\nRed Hat OpenJDK | \\n1.8.222 | \\n
Jenkins | \\nJenkins rpm | \\n2.176.2 | \\n
VSO의 새 인스턴스를 설치하려면 먼저 HashiCorp Helm Repo를 추가하고 Chart에 액세스할 수 있는지 확인한다:
$helm repo add hashicorp https://helm.releases.hashicorp.com
+"hashicorp" has been added to your repositories
+
+$ helm search repo hashicorp/vault-secrets-operator --devel
+NAME CHART VERSION APP VERSION DESCRIPTION
+hashicorp/vault-secrets-operator 0.1.0-beta 0.1.0-beta Official HashiCorp Vault Secrets Operator Chart
+
그런다음 Operator를 설치한다:
$ helm install --create-namespace --namespace vault-secrets-operator vault-secrets-operator hashicorp/vault-secrets-operator --version 0.1.0-beta
+
업그레이드는 기존 설치에서 helm upgrade
로 수행할 수 있다. 설치 또는 업그레이드 전에 항상 --dry-run
으로 헬름을 실행하여 변경 사항을 확인한다.
\\n\\n참고:
\\n
\\n현재 Vault 비밀 오퍼레이터는 공개 베타 버전입니다. *here*에서 GitHub 이슈를 개설하여 피드백을 제공해 주세요.
HCP Boundary 출시 (Public Beta)
\\nHashicorp Developer Site 출시 (Public Beta)
\\nConsul Service Mesh 에 대한 AWS Lambda 지원 (Public Beta)
\\nCDKTF (Cloud Development Kit for Terraform) General Available
\\nNomad: Nomad Variables and Service Discovery
\\n필수 Upgrade Version: Release Note 에서 * 표기된 Version 은 필수로 거쳐야 하는 Version (예: v202207-2, v202204-2)
PostgresSQL 버전 10 지원종료: TFE 에 대해 External PostgresSQL 사용하는 경우 최소 버전 12 이상으로 Upgrade 필요
구버전 OS 지원종료: 2023년 2월 release (v202302-1) 을 기점으로 아래 OS 목록에 대해 지원 종료
Debian 8, 9
Ubuntu 14.04, 16.04
Amazon Linux 2014.03, 2014.09, 2015.03, 2015.09, 2016.03, 2016.09, 2017.03, 2017.09, 2018.03
Hashiconf Global
\\nDay 1: ZTS (Zero Trust Security) 와 Cloud Service Networking 을 메인 주제로 새로운 기능과 HCP 서비스에 대한 소개
\\nDay 2: Infrastructure 및 Application 자동화 관련 제품군을 메인 주제로 새로운 기능 소개
\\n대상 OS
Debian 8, 9
Ubuntu 14.04, 16.04
Amazon Linux 2014.03, 2014.09, 2015.03, 2015.09, 2016.03, 2016.09, 2017.03, 2017.09, 2018.03
대상 Postgres
Terraform Run Tasks in Public Registry
\\n대상 OS
Debian 8, 9
Ubuntu 14.04, 16.04
Amazon Linux 2014.03, 2014.09, 2015.03, 2015.09, 2016.03, 2016.09, 2017.03, 2017.09, 2018.03
대상 Postgres
Dynamic Secrets for Waypoint with Vault
\\nv202302-1
) 을 기점으로 구버전 OS 및 Postrgres DB 에 대한 지원 종료 대상 OS
Debian 8, 9
Ubuntu 14.04, 16.04
Amazon Linux 2014.03, 2014.09, 2015.03, 2015.09, 2016.03, 2016.09, 2017.03, 2017.09, 2018.03
대상 Postgres
azure_site_recovery_replication_recovery_plan
customer managed key
및 public_network_access_enabled
설정 지원node_public_ip_tags
설정 지원google_iam_access_boundary_policy
, google_tags_location_tag_bindings
google_spanner_database_iam_member
및 google_spanner_instance_iam_member
설정 지원proxy-defaults
, service-default
) 수정envoy-ready-bind-port
및 envoy-ready-bind-address
설정 지원Terraform Cloud Adds ‘Projects’ to Organize Workspaces at Scale
\\nagent run pipeline mode
사용 시 실행 계획이 무기한 Queue 에 대기하는 문제 발생. 관련하여 tfe-task-worker
를 통해 [ERROR] core: Unexpected HTTP response code: method=POST url=https://terraform.example.com/api/agent/register status=404
라는 Error Log 출력되며 2023년 3월 Release (v202303-1
) 에서 해결 예정manage-workspaces
권한 부여 시, read-workspaces
도 부여되는 문제 발생. 더불어 manage-workspaces
에 대한 권한 제거 시 read-workspaces
권한은 제거 되지 않고 부여된 채로 유지 되는 문제 발생. 이후 출시 예정인 Release 에서 해결 예정google_data_catalog
, google_scc_mute_config
, google_workstations_workstation_config
Writing Terraform for unsupported resources
\\ntags
설정 지원snapshot_identifier
와 global_cluster_identifier
설정에 의한 잘못된 복원 수행 개선geo_backup_key_vault_key_id
와 geo_backup_user_assigned_identity_id
설정 지원allow_forwarded_traffic
, allow_gateway_transit
, use_remote_gateways
에 대한 기본값 설정hub_routing_preference
설정 지원auth_v2
, token_store_enabled
, ip_restriction
, scp_ip_restriction
관련 오류 개선auth_v2
, token_store_enabled
, ip_restriction
, scp_ip_restriction
관련 오류 개선auth_v2
, token_store_enabled
, ip_restriction
, scp_ip_restriction
관련 오류 개선auth_v2
, token_store_enabled
, ip_restriction
, scp_ip_restriction
관련 오류 개선is_case_insensitive
와 default_collation
설정 지원scratch_disk.size
와 local_nvme_ssd_block
설정 지원managed.dns_authorizations
관련 오류 개선enforce_on_key_name
관련 설정 오류 개선consul token update
명령어 수행 시 -append-policy-id
, -append-policy-name
, -append-role-id
, -append-service-identity
, -append-node-identity
매개변수 설정 지원HTTPRoute
관련 오류 개선namespace status
, quota status
, server members
명령어에 대해 -json
과 -t
매개변수 설정 지원Dynamic provider credentials now generally available for Terraform Cloud
\\n동적인증처리
를 지원합니다.launch_template
설정 추가role_last_used
설정 추가compatible_runtimes = python 3.10
설정 추가hosting_environment_id
설정 추가hosting_environment_id
설정 추가query_string
길이 제약 제거dynamic_criteria.0.ignore_data_before
미설정시 발생하는 오류 개선point_in_time_restore_time_in_utc
관련 오류 개선location
설정 required 로 변경 (기존: optional)\\inspect_job.actions.job_notification_emails
, inspect_job.actions.deidentify
, triggers.manual
그리고 inspect_job.storage_config.hybrid_options
설정 추가weekly_schedule
설정 optional 로 변경USE_ORIGIN_HEADERS
사용시 발생하는 TTL 관련 오류 개선Vault Secrets Operator: A new method for Kubernetes integration
\\ncreateMode
를 nil 대신 default 로 대체1.22.11
, 1.23.8
, 1.24.6
, 1.25.4
MaxEjectionPercent
과 BaseEjectionTime
설정 지원Terraform Cloud updates plans with an enhanced Free tier and more flexibility
\\nTerraform Cloud adds Vault-backed dynamic credentials
\\nv202308-1
부터 TFE 구동을 위한 Container 구성이 terraform-enterprise
라는 단일 Container 로 통합 (terraform plan
또는 terraform apply
는 기존 방식과 동일하게 수행 때마다 agent container 생성)v202308-1
부터 Docker 19.03 지원 종료 예정ap-east-1
region 에서 code_signing_config_arn
설정 지원forbidden_account_ids
처리 오류 개선InvalidParameterException: You cannot specify both rotation frequency and schedule expression together
오류 개선InvalidParameter: PrivateDnsOnlyForInboundResolverEndpoint not supported for this service
오류 개선public_network_access_enabled
설정 지원public_network_access_enabled
설정 지원allowed_origin_patterns
설정 지원cors
설정 시 allowed_origins
에 대한 설정 항목 수 오류 개선cors
설정 시 allowed_origins
에 대한 설정 항목 수 오류 개선google_compute_forwarding_rule
설정 내 no_automate_dns_zone
추가google_compute_disk_async_replication
정식 지원google_compute_disk
설정 내 async_primary_disk
추가google_compute_region_disk
설정 내 async_primary_disk
추가google_network_services_edge_cache_keyset
의 기본 timeout 값을 90 분으로 설정terraform-enterprise
라는 단일 Container 로 통합. terraform plan
또는 terraform apply
는 기존 방식과 동일하게 수행 때마다 agent container 생성 (적용시점: v202309-1
부터 적용)v202308-1
부터 적용)Manage Policy Overrides
에 대해 기본 부여된 정책 수정 (기존 Read -> List)terraform-enterprise
라는 단일 Container 로 통합. terraform plan
또는 terraform apply
는 기존 방식과 동일하게 수행 때마다 agent container 생성 (적용시점: v202309-1
부터 적용)dynamic provider credentials
에 대해 Workspace 내 Provider 당 설정 지원setting protocol: Invalid address to set
오류 수정tag propagation: timeout while waiting for state to become 'TRUE'
오류 개선terraform test
기능 추가 - 작성한 terraform code 에 대해 .tftest.hcl code 를 작성하여 검증 지원AWS_ENDPOINT_URL_DYNAMODB
, AWS_ENDPOINT_URL_IAM
, AWS_ENDPOINT_URL_S3
, AWS_ENDPOINT_URL_STS
us-east-1
로 설정invalid new value for .skip_destroy: was cty.False, but now null
오류 개선/
가 포함되는 것 허용본 문서는 HashiCorp 공식 GitHub의 Vault Secret Operator 저장소 에서 제공하는 코드를 활용하여 환경구성 및 샘플 애플리케이션 배포/연동에 대한 상세 분석을 제공한다.
# 저장소 복제
+$ git clone https://github.com/hashicorp/vault-secrets-operator.git
+
+# 작업 디렉토리 이동
+$ cd vault-secrets-operator
+
$ make setup-kind
+
setup-kind
수행 후 생성된 KinD 클러스터 및 파드정보 확인vault-secrets-operator-control-plane
가 단일노드로 배포된 것을 확인할 수 있다.
$ kubectl get nodes -o wide
+NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
+vault-secrets-operator-control-plane Ready control-plane 3m18s v1.25.3 172.18.0.2 <none> Ubuntu 22.04.1 LTS 5.15.49-linuxkit containerd://1.6.9
+
+$ kubectl get pods -A
+NAMESPACE NAME READY STATUS RESTARTS AGE
+kube-system coredns-565d847f94-42vpm 1/1 Running 0 3m5s
+kube-system coredns-565d847f94-6fsv9 1/1 Running 0 3m5s
+kube-system etcd-vault-secrets-operator-control-plane 1/1 Running 0 3m18s
+kube-system kindnet-9j486 1/1 Running 0 3m6s
+kube-system kube-apiserver-vault-secrets-operator-control-plane 1/1 Running 0 3m18s
+kube-system kube-controller-manager-vault-secrets-operator-control-plane 1/1 Running 0 3m18s
+kube-system kube-proxy-tfqc8 1/1 Running 0 3m6s
+kube-system kube-scheduler-vault-secrets-operator-control-plane 1/1 Running 0 3m17s
+local-path-storage local-path-provisioner-684f458cdd-2dzfn 1/1 Running 0 3m5s
+
앞서 생성된 KinD 클러스터 내부에 Vault 클러스터를 배포한다. 이때, 필요한 사전 환경을 Terraform 코드를 통해 자동으로 구성한다.
make setup-integration-test
+
# Pod 확인
+$ kubectl get pods -n vault
+NAME READY STATUS RESTARTS AGE
+vault-0 1/1 Running 0 73s
+
+# vault 상태확인
+$ kubectl exec -n vault -it vault-0 -- vault status
+Key Value
+--- -----
+Seal Type shamir
+Initialized true
+Sealed false
+Total Shares 1
+Threshold 1
+Version 1.13.2
+Build Date 2023-04-25T13:02:50Z
+Storage Type inmem
+Cluster Name vault-cluster-199af322
+Cluster ID 23b647d5-f067-ba94-b359-2fca26af9ff9
+HA Enabled false
+
Terraform의 kubernetes
, helm
프로바이더를 사용하여 다음과 같은 리소스를 자동으로 배포한다.
setup.sh
스크립트를 실행하여 다음 3가지 시크릿 엔진에 대한 실습 환경을 구성한다.
$ ./config/samples/setup.sh
+
KV 시크릿엔진 Version 1, Version2를 활성화 하고 샘플 데이터를 주입한다.
vault secrets disable kvv2/
+vault secrets enable -path=kvv2 kv-v2
+vault kv put kvv2/secret username="db-readonly-username" password="db-secret-password"
+
+vault secrets disable kvv1/
+vault secrets enable -path=kvv1 -version=1 kv
+vault kv put kvv1/secret username="v1-user" password="v1-password"
+
PKI 시크릿 엔진을 활성화하고 다음 설정을 진행한다.
# PKI Secret 엔진 활성화
+vault secrets disable pki
+vault secrets enable pki
+
+# PKI 인증서 생성
+vault write pki/root/generate/internal \\
+ common_name=example.com \\
+ ttl=768h
+
+# 설정
+vault write pki/config/urls \\
+ issuing_certificates="http://127.0.0.1:8200/v1/pki/ca" \\
+ crl_distribution_points="http://127.0.0.1:8200/v1/pki/crl"
+
+# 역할구성
+vault write pki/roles/default \\
+ allowed_domains=example.com \\
+ allowed_domains=localhost \\
+ allow_subdomains=true \\
+ max_ttl=72h
+
각 시크릿 엔진에 대한 ACL Policy를 정의하기 위해 다음 hcl
을 작성하고 적용한다.
# policy.hcl 작성
+cat <<EOT > /tmp/policy.hcl
+path "kvv2/*" {
+ capabilities = ["read"]
+}
+path "kvv1/*" {
+ capabilities = ["read"]
+}
+path "pki/*" {
+ capabilities = ["read", "create", "update"]
+}
+EOT
+
+# demo 정책 생성
+vault policy write demo /tmp/policy.hcl
+
vault policy write
명령으로 정책을 생성하고 확인한다.
Vault와 연동을 위해 kubernetes 인증방식을 설정한다.
참고:
Beta 버전에서는 Kubernetes 인증 방식만 제공
# Kubernetes 인증방식 활성화
+vault auth disable kubernetes
+vault auth enable kubernetes
+
+vault write auth/kubernetes/config \\
+ kubernetes_host=https://kubernetes.default.svc
+
+vault write auth/kubernetes/role/demo \\
+ bound_service_account_names=default \\
+ bound_service_account_namespaces=tenant-1,tenant-2 \\
+ policies=demo \\
+ ttl=1h
+
VSO에서는 현재 Kubernetes 인증 방식만을 제공하고 있으므로 Kubernetes 인증 방식을 통해 실습을 진행한다.
kubernetes 인증방식 구성을 위해 Roles, Config를 정의한다.
default
tenant-1,tenant-2
demo
1h
(3600s)https://kubernetes.default.svc
(참고) Entity 확인
K8s 인증방식의 역할(Role)에서 사용할 네임스페이스 확인
kubectl get ns | grep tenant
+tenant-1 Active 5h2m
+tenant-2 Active 5h2m
+
Vault 설정이 완료되었으므로 실제 Kubernetes Cluster에서 Operator를 배포한다.
$ make build docker-build deploy-kind
+
$ kubectl get pods -n vault-secrets-operator-system
+NAME READY STATUS RESTARTS AGE
+vault-secrets-operator-controller-manager-6f8b6b8f49-5lt97 2/2 Running 0 3h59m
+
+$ k get crd -A
+NAME CREATED AT
+vaultauths.secrets.hashicorp.com 2023-05-12T08:37:15Z
+vaultconnections.secrets.hashicorp.com 2023-05-12T08:37:15Z
+vaultdynamicsecrets.secrets.hashicorp.com 2023-05-12T08:37:15Z
+vaultpkisecrets.secrets.hashicorp.com 2023-05-12T08:37:15Z
+vaultstaticsecrets.secrets.hashicorp.com 2023-05-12T08:37:15Z
+
$ kubectl apply -k config/samples
+
+secret/pki1 created
+secret/secret1 created
+secret/secret1 created
+service/tls-app-service created
+ingress.networking.k8s.io/tls-example-ingress created
+vaultauth.secrets.hashicorp.com/vaultauth-sample created
+vaultauth.secrets.hashicorp.com/vaultauth-sample created
+vaultconnection.secrets.hashicorp.com/vaultconnection-sample created
+vaultconnection.secrets.hashicorp.com/vaultconnection-sample created
+vaultdynamicsecret.secrets.hashicorp.com/vaultdynamicsecret-sample created
+vaultpkisecret.secrets.hashicorp.com/vaultpkisecret-sample-tenant-1 created
+vaultpkisecret.secrets.hashicorp.com/vaultpkisecret-tls created
+vaultstaticsecret.secrets.hashicorp.com/vaultstaticsecret-sample-tenant-1 created
+vaultstaticsecret.secrets.hashicorp.com/vaultstaticsecret-sample-tenant-2 created
+pod/app1 created
+pod/tls-app created
+pod/app1 created
+
$ kubectl get secrets -n tenant-1 secret1 -o yaml
+$ kubectl get secrets -n tenant-1 pki1 -o yaml
+$ kubectl get secrets -n tenant-2 secret1 -o yaml
+
설명추가
VaultConnection
커스텀 리소스Vault Operator가 연결할 Vault Cluster 정보를 구성한다.
`,12),I=n("code",null,".spec.address",-1),j={href:"http://vault.vault.svc.cluster.local:8200",target:"_blank",rel:"noopener noreferrer"},E=t(`---
+apiVersion: secrets.hashicorp.com/v1alpha1
+kind: VaultConnection
+metadata:
+ labels:
+ app.kubernetes.io/name: vaultconnection
+ app.kubernetes.io/instance: vaultconnection-sample
+ app.kubernetes.io/part-of: vault-secrets-operator
+ app.kubernetes.io/managed-by: kustomize
+ app.kubernetes.io/created-by: vault-secrets-operator
+ name: vaultconnection-sample
+ namespace: tenant-1
+spec:
+ address: http://vault.vault.svc.cluster.local:8200
+---
+apiVersion: secrets.hashicorp.com/v1alpha1
+kind: VaultConnection
+metadata:
+ labels:
+ app.kubernetes.io/name: vaultconnection
+ app.kubernetes.io/instance: vaultconnection-sample
+ app.kubernetes.io/part-of: vault-secrets-operator
+ app.kubernetes.io/managed-by: kustomize
+ app.kubernetes.io/created-by: vault-secrets-operator
+ name: vaultconnection-sample
+ namespace: tenant-2
+spec:
+ address: http://vault.vault.svc.cluster.local:8200
+
VaultAuth
커스텀 리소스사전에 정의된 VaultConnection
을 통해 Operator가 Vault Server와 연결할 때, 어떤 인증방식을 사용할지 구성한다.
참고 : Beta 버전에서는 K8s 인증방식만 제공
.spec.vaultConnectionRef
.spec.method
.spec.mount
.spec.kubernetes.role
.spec.kubernetes.serviceAccount
---
+apiVersion: secrets.hashicorp.com/v1alpha1
+kind: VaultAuth
+metadata:
+ labels:
+ app.kubernetes.io/name: vaultauth
+ app.kubernetes.io/instance: vaultauth-sample
+ app.kubernetes.io/part-of: vault-secrets-operator
+ app.kubernetes.io/managed-by: kustomize
+ app.kubernetes.io/created-by: vault-secrets-operator
+ name: vaultauth-sample
+ namespace: tenant-1
+spec:
+ vaultConnectionRef: vaultconnection-sample
+ method: kubernetes
+ mount: kubernetes
+ kubernetes:
+ role: demo
+ serviceAccount: default
+---
+apiVersion: secrets.hashicorp.com/v1alpha1
+kind: VaultAuth
+metadata:
+ labels:
+ app.kubernetes.io/name: vaultauth
+ app.kubernetes.io/instance: vaultauth-sample
+ app.kubernetes.io/part-of: vault-secrets-operator
+ app.kubernetes.io/managed-by: kustomize
+ app.kubernetes.io/created-by: vault-secrets-operator
+ name: vaultauth-sample
+ namespace: tenant-2
+spec:
+ vaultConnectionRef: vaultconnection-sample
+ method: kubernetes
+ mount: kubernetes
+ kubernetes:
+ role: demo
+ serviceAccount: default
+
VSO에서 제공하는 3가지 CRD를 사용하여 Kubernetes 오브젝트와 연동하여 사용하는 방법을 알아본다.
VaultPKISecret
: Pod + PKI Secret다음은 PKI 인증서를 생성하고 Nginx 웹 서버에 적용하는 실습 예제이다. Nginx 파드를 생성할 때 secret 타입의 볼륨을 마운트한다.
VaultPKISecret
---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: pki1
+ namespace: tenant-1
+type: Opaque
+---
+apiVersion: secrets.hashicorp.com/v1alpha1
+kind: VaultPKISecret
+metadata:
+ namespace: tenant-1
+ name: vaultpkisecret-sample-tenant-1
+spec:
+ vaultAuthRef: vaultauth-sample
+ namespace: tenant-1
+ mount: pki
+ name: default
+ destination:
+ name: pki1
+ commonName: consul.example.com
+ format: pem
+ revoke: true
+ clear: true
+ expiryOffset: 5s
+ ttl: 15s
+
---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: app1
+ namespace: tenant-1
+spec:
+ containers:
+ - name: nginx
+ image: nginx
+ volumeMounts:
+ - name: secrets
+ mountPath: "/etc/secrets"
+ readOnly: true
+ volumes:
+ - name: secrets
+ secret:
+ # created in Terraform
+ secretName: pki1
+ optional: false # default setting; "mysecret" must exist
+
실제 PKI 인증서가 정상적으로 생성되는 확인해본다.
/etc/secrets
디렉토에서 파일목록 확인$ ls -lrt /etc/secrets
+
+total 0
+lrwxrwxrwx 1 root root 20 May 14 08:33 serial_number -> ..data/serial_number
+lrwxrwxrwx 1 root root 23 May 14 08:33 private_key_type -> ..data/private_key_type
+lrwxrwxrwx 1 root root 18 May 14 08:33 private_key -> ..data/private_key
+lrwxrwxrwx 1 root root 17 May 14 08:33 issuing_ca -> ..data/issuing_ca
+lrwxrwxrwx 1 root root 17 May 14 08:33 expiration -> ..data/expiration
+lrwxrwxrwx 1 root root 18 May 14 08:33 certificate -> ..data/certificate
+lrwxrwxrwx 1 root root 15 May 14 08:33 ca_chain -> ..data/ca_chain
+lrwxrwxrwx 1 root root 11 May 14 08:33 _raw -> ..data/_raw
+
본 실습에서는 실제 nginx 파드의 구성파일에 PKI 인증서를 적용하는 시나리오가 아닌 단순 파일생성 및 갱신해보았다.
VaultPKISecret
예제2 : Ingress + Pod + PKI Secret이번 실습에서는 앞서 확인한 PKI 인증서를 활용하여 K8s Ingress 오브젝트에 적용하고 주기적으로 교체되는 시나리오를 확인해본다.
`,21),N={href:"https://github.com/hashicorp/vault-secrets-operator/tree/main#ingress-tls-with-vaultpkisecret",target:"_blank",rel:"noopener noreferrer"},M=t(`kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
+
+kubectl wait --namespace ingress-nginx \\
+ --for=condition=ready pod \\
+ --selector=app.kubernetes.io/component=controller \\
+ --timeout=90s
+
---
+apiVersion: secrets.hashicorp.com/v1alpha1
+kind: VaultPKISecret
+metadata:
+ name: vaultpkisecret-tls
+ namespace: tenant-1
+spec:
+ vaultAuthRef: vaultauth-sample
+ namespace: tenant-1
+ mount: pki
+ name: default
+ destination:
+ create: true
+ name: pki-tls
+ type: kubernetes.io/tls
+ commonName: localhost
+ format: pem
+ revoke: true
+ clear: true
+ expiryOffset: 15s
+ ttl: 1m
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: tls-app
+ namespace: tenant-1
+ labels:
+ app: tls-app
+spec:
+ containers:
+ - command:
+ - /agnhost
+ - netexec
+ - --http-port
+ - "8080"
+ image: registry.k8s.io/e2e-test-images/agnhost:2.39
+ name: tls-app
+---
+kind: Service
+apiVersion: v1
+metadata:
+ name: tls-app-service
+ namespace: tenant-1
+spec:
+ selector:
+ app: tls-app
+ ports:
+ - port: 443
+ targetPort: 8080
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: tls-example-ingress
+ namespace: tenant-1
+ annotations:
+ nginx.ingress.kubernetes.io/rewrite-target: /$2
+spec:
+ tls:
+ - hosts:
+ - localhost
+ secretName: pki-tls
+ rules:
+ - host: localhost
+ http:
+ paths:
+ - path: /tls-app(/|$)(.*)
+ pathType: Prefix
+ backend:
+ service:
+ name: tls-app-service
+ port:
+ number: 443
+
$ curl -k https://localhost:38443/tls-app/hostname
+tls-app%
+$ curl -kvI https://localhost:38443/tls-app/hostname
+* Trying 127.0.0.1:38443...
+* Connected to localhost (127.0.0.1) port 38443 (#0)
+# 중략
+* Server certificate:
+* subject: CN=localhost
+* start date: May 14 08:04:00 2023 GMT
+* expire date: May 14 08:05:30 2023 GMT
+* issuer: CN=example.com
+
kubectl logs -f -n ingress-nginx -l app.kubernetes.io/instance=ingress-nginx
+W0514 07:51:58.673604 1 client_config.go:615] Neither --kubeconfig nor --master was specified. Using the inClusterConfig. This might not work.
+{"level":"info","msg":"patching webhook configurations 'ingress-nginx-admission' mutating=false, validating=true, failurePolicy=Fail","source":"k8s/k8s.go:118","time":"2023-05-14T07:51:58Z"}
+{"level":"info","msg":"Patched hook(s)","source":"k8s/k8s.go:138","time":"2023-05-14T07:51:58Z"}
+I0514 08:19:30.110926 9 store.go:619] "secret was updated and it is used in ingress annotations. Parsing" secret="tenant-1/pki-tls"
+I0514 08:19:30.113988 9 backend_ssl.go:59] "Updating secret in local store" name="tenant-1/pki-tls"
+W0514 08:19:30.114178 9 controller.go:1406] SSL certificate for server "localhost" is about to expire (2023-05-14 08:20:30 +0000 UTC)
+I0514 08:20:15.208102 9 store.go:619] "secret was updated and it is used in ingress annotations. Parsing" secret="tenant-1/pki-tls"
+I0514 08:20:15.208539 9 backend_ssl.go:59] "Updating secret in local store" name="tenant-1/pki-tls"
+W0514 08:20:15.208801 9 controller.go:1406] SSL certificate for server "localhost" is about to expire (2023-05-14 08:21:15 +0000 UTC)
+W0514 08:20:18.543113 9 controller.go:1406] SSL certificate for server "localhost" is about to expire (2023-05-14 08:21:15 +0000 UTC)
+I0514 08:21:00.107794 9 store.go:619] "secret was updated and it is used in ingress annotations. Parsing" secret="tenant-1/pki-tls"
+I0514 08:21:00.108127 9 backend_ssl.go:59] "Updating secret in local store" name="tenant-1/pki-tls"
+W0514 08:21:00.108295 9 controller.go:1406] SSL certificate for server "localhost" is about to expire (2023-05-14 08:22:00 +0000 UTC)
+W0514 07:51:58.418022 1 client_config.go:615] Neither --kubeconfig nor --master was specified. Using the inClusterConfig. This might not work.
+{"err":"secrets \\"ingress-nginx-admission\\" not found","level":"info","msg":"no secret found","source":"k8s/k8s.go:229","time":"2023-05-14T07:51:58Z"}
+{"level":"info","msg":"creating new secret","source":"cmd/create.go:28","time":"2023-05-14T07:51:58Z"}
+
VaultStaticSecret
예제 :---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: secret1
+ namespace: tenant-1
+type: Opaque
+---
+apiVersion: secrets.hashicorp.com/v1alpha1
+kind: VaultStaticSecret
+metadata:
+ namespace: tenant-1
+ name: vaultstaticsecret-sample-tenant-1
+spec:
+ # namespace: cluster1/tenant-1
+ vaultAuthRef: vaultauth-sample
+ mount: kvv2
+ type: kv-v2
+ name: secret
+ refreshAfter: 5s
+ destination:
+ name: secret1
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: secret1
+ namespace: tenant-2
+type: Opaque
+---
+apiVersion: secrets.hashicorp.com/v1alpha1
+kind: VaultStaticSecret
+metadata:
+ namespace: tenant-2
+ name: vaultstaticsecret-sample-tenant-2
+spec:
+ # namespace: cluster1/tenant-2
+ vaultAuthRef: vaultauth-sample
+ mount: kvv1
+ type: kv-v1
+ name: secret
+ refreshAfter: 5s
+ destination:
+ name: secret1
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: app1
+ namespace: tenant-1
+spec:
+ containers:
+ - name: nginx
+ image: nginx
+ volumeMounts:
+ - name: secrets
+ mountPath: "/etc/secrets"
+ readOnly: true
+ volumes:
+ - name: secrets
+ secret:
+ secretName: secret1
+ optional: false # default setting; "mysecret" must exist
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: app1
+ namespace: tenant-2
+spec:
+ containers:
+ - name: nginx
+ image: nginx
+ volumeMounts:
+ - name: secrets
+ mountPath: "/etc/secrets"
+ readOnly: true
+ volumes:
+ - name: secrets
+ secret:
+ secretName: secret1
+ optional: false # default setting; "mysecret" must exist
+
VaultDynamicSecret
🔥 업데이트 예정
apiVersion: secrets.hashicorp.com/v1alpha1
+kind: VaultDynamicSecret
+metadata:
+ labels:
+ app.kubernetes.io/name: vaultdynamicsecret
+ app.kubernetes.io/instance: vaultdynamicsecret-sample
+ app.kubernetes.io/part-of: vault-secrets-operator
+ app.kubernetes.io/managed-by: kustomize
+ app.kubernetes.io/created-by: vault-secrets-operator
+ name: vaultdynamicsecret-sample
+spec:
+ # TODO(user): Add fields here
+
샘플 삭제:
# K8s 리소스 삭제
+$ kubectl delete -k config/samples
+
+# kind 클러스터 삭제
+$ kind delete clusters vault-secrets-operator
+
\\n\\n📌 참고:
\\n
\\n현재 Vault 비밀 오퍼레이터는 공개 베타 버전입니다. *here*에서 GitHub 이슈를 개설하여 피드백을 제공해 주세요.
본 문서는 HashiCorp 공식 GitHub의 Vault Secret Operator 저장소 에서 제공하는 코드를 활용하여 환경구성 및 샘플 애플리케이션 배포/연동에 대한 상세 분석을 제공한다.
"}');export{U as comp,H as data}; diff --git a/assets/400-error.html-De4cfBHo.js b/assets/400-error.html-De4cfBHo.js new file mode 100644 index 0000000000..5bedaa50fe --- /dev/null +++ b/assets/400-error.html-De4cfBHo.js @@ -0,0 +1,45 @@ +import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{r as o,o as e,c as p,b as n,d as s,a as r,e as c}from"./app-Bzk8Nrll.js";const l={},i=n("h1",{id:"vault-400-error",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#vault-400-error"},[n("span",null,"Vault 400 Error")])],-1),u={href:"https://www.vaultproject.io/api#http-status-codes",target:"_blank",rel:"noopener noreferrer"},d=c(`Vault에 API 요청시 400에러가 발생하는 경우 Vault로 전달된 데이터 형태가 올바른지 확인이 필요하다.
400
: Invalid request, missing or invalid data.예를들어 아래와 같이 Transit
의 복호화 요청을 하는 경우 데이터가 비어있다면 응답과 Audit로그에서 400 에러관련 메세지를 확인할 수 있다.
1 error occurred: * invalid request
curl \\
+ -H "X-Vault-Token: s.HeeRXjkW1KJhF8ofQsglI9yw" \\
+ -X POST \\
+ -d "{}" \\
+ http://192.168.60.103:8200/v1/transit/decrypt/my-key
+
{
+ "time": "2022-03-04T08:02:37.596190958Z",
+ "type": "response",
+ "auth": {
+ "client_token": "hmac-sha256:17bc16e3346dd6c398646cb7da8e0bd71ae720f608a8c447b8942b8283388600",
+ "accessor": "hmac-sha256:798bb09d10dc2ac18533acb3d049c4185af3328fbc88fedd23081f63caa13b44",
+ "display_name": "root",
+ "policies": [
+ "root"
+ ],
+ "token_policies": [
+ "root"
+ ],
+ "token_type": "service",
+ "token_issue_time": "2021-09-15T13:45:47+09:00"
+ },
+ "request": {
+ "id": "c39906c5-f48d-c177-37c2-4c1635db78e7",
+ "operation": "update",
+ "mount_type": "transit",
+ "client_token": "hmac-sha256:17bc16e3346dd6c398646cb7da8e0bd71ae720f608a8c447b8942b8283388600",
+ "client_token_accessor": "hmac-sha256:798bb09d10dc2ac18533acb3d049c4185af3328fbc88fedd23081f63caa13b44",
+ "namespace": {
+ "id": "root"
+ },
+ "path": "kbhealth-transit/prod/decrypt/aes256",
+ "data": {
+ "ciphertext": "hmac-sha256:34c2966e2ef36e2dcdb24f05fd4442b8f85c0d2fbf0887977636c7592e2cef3b"
+ },
+ "remote_address": "10.100.0.85"
+ },
+ "response": {
+ "mount_type": "transit",
+ "data": {
+ "error": "hmac-sha256:bf7d730e400653f79b134c3bdb593f8220f6f1588a26048a6e1272a01ad47384"
+ }
+ },
+ "error": "1 error occurred:\\n\\t* invalid request\\n\\n"
+}
+
\\n\\nVault HTTP Status Codes : https://www.vaultproject.io/api#http-status-codes
\\n
Vault에 API 요청시 400에러가 발생하는 경우 Vault로 전달된 데이터 형태가 올바른지 확인이 필요하다.
\\n400
: Invalid request, missing or invalid data.404 Not Found
\\n","autoDesc":true}');export{d as comp,u as data}; diff --git a/assets/AlibabaCloud.html-Ddvy_KKt.js b/assets/AlibabaCloud.html-Ddvy_KKt.js new file mode 100644 index 0000000000..66647aa135 --- /dev/null +++ b/assets/AlibabaCloud.html-Ddvy_KKt.js @@ -0,0 +1,93 @@ +import{_ as t}from"./plugin-vue_export-helper-DlAUqK2U.js";import{r as e,o as p,c as o,b as n,d as s,a as l,e as c}from"./app-Bzk8Nrll.js";const i={},r=n("h1",{id:"alibaba-cloud-packer-sample",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#alibaba-cloud-packer-sample"},[n("span",null,"Alibaba Cloud Packer Sample")])],-1),u=n("h2",{id:"packer-pkr-hcl",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#packer-pkr-hcl"},[n("span",null,"packer.pkr.hcl")])],-1),d=n("code",null,"vault()",-1),k={href:"https://www.packer.io/docs/templates/hcl_templates/functions/contextual/vault",target:"_blank",rel:"noopener noreferrer"},m=c(`# packer build -force .
+
+locals {
+ access_key = vault("/kv-v2/data/alicloud", "access_key")
+ secret_key = vault("/kv-v2/data/alicloud", "secret_key")
+}
+
+variable "region" {
+ default = "ap-southeast-1"
+ description = "https://www.alibabacloud.com/help/doc-detail/40654.htm"
+}
+
+source "alicloud-ecs" "basic-example" {
+ access_key = local.access_key
+ secret_key = local.secret_key
+ region = var.region
+ image_name = "ssh_otp_image_1_5"
+ source_image = "centos_7_9_x64_20G_alibase_20210623.vhd"
+ ssh_username = "root"
+ instance_type = "ecs.n1.tiny"
+ io_optimized = true
+ internet_charge_type = "PayByTraffic"
+ image_force_delete = true
+}
+
+build {
+ sources = ["sources.alicloud-ecs.basic-example"]
+
+ provisioner "file" {
+ source = "./files/"
+ destination = "/tmp"
+ }
+
+# Vault OTP
+ provisioner "shell" {
+ inline = [
+ "cp /tmp/sshd /etc/pam.d/sshd",
+ "cp /tmp/sshd_config /etc/ssh/sshd_config",
+ "mkdir -p /etc/vault.d",
+ "cp /tmp/vault.hcl /etc/vault.d/vault.hcl",
+ "cp /tmp/vault-ssh-helper /usr/bin/vault-ssh-helper",
+ "/usr/bin/vault-ssh-helper -verify-only -config=/etc/vault.d/vault.hcl -dev",
+ "sudo adduser test",
+ "echo password | passwd --stdin test",
+ "echo 'test ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers",
+ "sudo sed -ie 's/SELINUX=enforcing/SELINUX=disabled /g' /etc/selinux/config"
+ ]
+ }
+
+# Apache
+ provisioner "shell" {
+ inline = [
+ "sudo yum -y update",
+ "sleep 15",
+ "sudo yum -y update",
+ "sudo yum -y install httpd",
+ "sudo systemctl enable httpd",
+ "sudo systemctl start httpd",
+ "chmod +x /tmp/deploy_app.sh",
+ "PLACEHOLDER=${var.placeholder} WIDTH=600 HEIGHT=800 PREFIX=gs /tmp/deploy_app.sh",
+ # "sudo firewall-cmd --zone=public --permanent --add-port=80/tcp",
+ # "sudo firewall-cmd --reload",
+ ]
+ }
+}
+
+variable "placeholder" {
+ default = "placekitten.com"
+ description = "Image-as-a-service URL. Some other fun ones to try are fillmurray.com, placecage.com, placebeard.it, loremflickr.com, baconmockup.com, placeimg.com, placebear.com, placeskull.com, stevensegallery.com, placedog.net"
+}
+
#!/bin/bash
+# Script to deploy a very simple web application.
+# The web app has a customizable image and some text.
+
+cat << EOM > /var/www/html/index.html
+<html>
+ <head><title>Meow!</title></head>
+ <body>
+ <div style="width:800px;margin: 0 auto">
+
+ <!-- BEGIN -->
+ <center><img src="http://\${PLACEHOLDER}/\${WIDTH}/\${HEIGHT}"></img></center>
+ <center><h2>Meow World!</h2></center>
+ Welcome to \${PREFIX}'s app. Replace this text with your own.
+ <!-- END -->
+
+ </div>
+ </body>
+</html>
+EOM
+
+echo "Script complete."
+
vault()
는 vault 연동시 사용가능 : https://www.packer.io/docs/templates/hcl_templates/functions/contextual/vault# packer build -force .\\n\\nlocals {\\n access_key = vault(\\"/kv-v2/data/alicloud\\", \\"access_key\\")\\n secret_key = vault(\\"/kv-v2/data/alicloud\\", \\"secret_key\\")\\n}\\n\\nvariable \\"region\\" {\\n default = \\"ap-southeast-1\\"\\n description = \\"https://www.alibabacloud.com/help/doc-detail/40654.htm\\"\\n}\\n\\nsource \\"alicloud-ecs\\" \\"basic-example\\" {\\n access_key = local.access_key\\n secret_key = local.secret_key\\n region = var.region\\n image_name = \\"ssh_otp_image_1_5\\"\\n source_image = \\"centos_7_9_x64_20G_alibase_20210623.vhd\\"\\n ssh_username = \\"root\\"\\n instance_type = \\"ecs.n1.tiny\\"\\n io_optimized = true\\n internet_charge_type = \\"PayByTraffic\\"\\n image_force_delete = true\\n}\\n\\nbuild {\\n sources = [\\"sources.alicloud-ecs.basic-example\\"]\\n\\n provisioner \\"file\\" {\\n source = \\"./files/\\"\\n destination = \\"/tmp\\"\\n }\\n\\n# Vault OTP\\n provisioner \\"shell\\" {\\n inline = [\\n \\"cp /tmp/sshd /etc/pam.d/sshd\\",\\n \\"cp /tmp/sshd_config /etc/ssh/sshd_config\\",\\n \\"mkdir -p /etc/vault.d\\",\\n \\"cp /tmp/vault.hcl /etc/vault.d/vault.hcl\\",\\n \\"cp /tmp/vault-ssh-helper /usr/bin/vault-ssh-helper\\",\\n \\"/usr/bin/vault-ssh-helper -verify-only -config=/etc/vault.d/vault.hcl -dev\\",\\n \\"sudo adduser test\\",\\n \\"echo password | passwd --stdin test\\",\\n \\"echo 'test ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers\\",\\n \\"sudo sed -ie 's/SELINUX=enforcing/SELINUX=disabled /g' /etc/selinux/config\\"\\n ]\\n }\\n\\n# Apache\\n provisioner \\"shell\\" {\\n inline = [\\n \\"sudo yum -y update\\",\\n \\"sleep 15\\",\\n \\"sudo yum -y update\\",\\n \\"sudo yum -y install httpd\\",\\n \\"sudo systemctl enable httpd\\",\\n \\"sudo systemctl start httpd\\",\\n \\"chmod +x /tmp/deploy_app.sh\\",\\n \\"PLACEHOLDER=${var.placeholder} WIDTH=600 HEIGHT=800 PREFIX=gs /tmp/deploy_app.sh\\",\\n # \\"sudo firewall-cmd --zone=public --permanent --add-port=80/tcp\\",\\n # \\"sudo firewall-cmd --reload\\",\\n ]\\n }\\n}\\n\\nvariable \\"placeholder\\" {\\n default = \\"placekitten.com\\"\\n description = \\"Image-as-a-service URL. Some other fun ones to try are fillmurray.com, placecage.com, placebeard.it, loremflickr.com, baconmockup.com, placeimg.com, placebear.com, placeskull.com, stevensegallery.com, placedog.net\\"\\n}\\n
# packer init -upgrade .
+# packer build -force .
+
+locals {
+ client_id = vault("/kv/data/azure", "client_id")
+ client_secret = vault("/kv/data/azure", "client_secret")
+ tenant_id = vault("/kv/data/azure", "tenant_id")
+ subscription_id = vault("/kv/data/azure", "subscription_id")
+ resource_group_name = var.resource_name
+ virtual_network_name = "kbid-d-krc-vnet-002"
+ virtual_network_subnet_name = "d-mgmt-snet-001"
+ virtual_network_resource_group_name = "kbid-d-krc-mgmt-rg"
+ timestamp = formatdate("YYYYMMDD_hhmmss", timeadd(timestamp(), "9h")) #생성되는 이미지 이름을 time 기반으로 생성
+}
+
+variable "placeholder" {
+ default = "placekitten.com"
+ description = "Image-as-a-service URL. Some other fun ones to try are fillmurray.com, placecage.com, placebeard.it, loremflickr.com, baconmockup.com, placeimg.com, placebear.com, placeskull.com, stevensegallery.com, placedog.net"
+}
+
+# Basic example : https://www.packer.io/docs/builders/azure/arm#basic-example
+# MS Guide : https://docs.microsoft.com/ko-kr/azure/virtual-machines/linux/build-image-with-packer
+source "azure-arm" "basic-example" {
+ client_id = local.client_id
+ client_secret = local.client_secret
+ subscription_id = local.subscription_id
+ tenant_id = local.tenant_id
+
+ # shared_image_gallery {
+ # subscription = local.subscription_id
+ # resource_group = "myrg"
+ # gallery_name = "GalleryName"
+ # image_name = "gs_pkr_\${local.timestamp}"
+ # image_version = "1.0.0"
+ # }
+ managed_image_resource_group_name = local.resource_group_name
+ managed_image_name = "${var.image_name}-${local.timestamp}"
+
+ os_type = "Linux"
+ # az vm image list-publishers --location koreacentral --output table
+ image_publisher = "RedHat"
+ # az vm image list-offers --location koreacentral --publisher RedHat --output table
+ image_offer = "RHEL"
+ # az vm image list-skus --location koreacentral --publisher RedHat --offer RHEL --output table
+ image_sku = "8_4"
+
+ azure_tags = {
+ dept = "KBHC Terraform POC"
+ }
+
+ # az vm list-skus --location koreacentral --all --output table
+ build_resource_group_name = local.resource_group_name
+
+ #########################################
+ # 기존 생성되어있는 network 를 사용하기 위한 항목 #
+ #########################################
+ virtual_network_name = local.virtual_network_name
+ virtual_network_subnet_name = local.virtual_network_subnet_name
+ virtual_network_resource_group_name = local.virtual_network_resource_group_name
+
+ # location = "koreacentral"
+ vm_size = "Standard_A2_v2"
+}
+
+build {
+ sources = ["sources.azure-arm.basic-example"]
+
+ provisioner "file" {
+ source = "./files/"
+ destination = "/tmp"
+ }
+
+# Vault OTP
+ provisioner "shell" {
+ inline = [
+ "sudo cp /tmp/sshd /etc/pam.d/sshd",
+ "sudo cp /tmp/sshd_config /etc/ssh/sshd_config",
+ "sudo mkdir -p /etc/vault.d",
+ "sudo cp /tmp/vault.hcl /etc/vault.d/vault.hcl",
+ "sudo cp /tmp/vault-ssh-helper /usr/bin/vault-ssh-helper",
+ "echo \\"=== Vault_Check ===\\"",
+ "curl http://10.0.9.10:8200",
+ "/usr/bin/vault-ssh-helper -verify-only -config=/etc/vault.d/vault.hcl -dev",
+ "echo \\"=== Add User ===\\"",
+ "sudo adduser jboss",
+ "echo password | sudo passwd --stdin jboss",
+ "echo 'jboss ALL=(ALL) NOPASSWD: ALL' | sudo tee -a /etc/sudoers",
+ "echo \\"=== SELINUX DISABLE ===\\"",
+ "sudo sed -ie 's/SELINUX=enforcing/SELINUX=disabled /g' /etc/selinux/config"
+ ]
+ }
+
+# Apache
+ provisioner "shell" {
+ inline = [
+ "sudo yum -y update",
+ "sleep 15",
+ "sudo yum -y update",
+ "sudo yum -y install httpd",
+ "sudo systemctl enable httpd",
+ "sudo systemctl start httpd",
+ "chmod +x /tmp/deploy_app.sh",
+ "sudo PLACEHOLDER=${var.placeholder} WIDTH=600 HEIGHT=800 PREFIX=gs /tmp/deploy_app.sh",
+ "sudo firewall-cmd --zone=public --permanent --add-port=80/tcp",
+ "sudo firewall-cmd --reload",
+ ]
+ }
+}
+
#!/bin/bash
+# Script to deploy a very simple web application.
+# The web app has a customizable image and some text.
+
+cat << EOM > /var/www/html/index.html
+<html>
+ <head><title>Meow!</title></head>
+ <body>
+ <div style="width:800px;margin: 0 auto">
+
+ <!-- BEGIN -->
+ <center><img src="http://\${PLACEHOLDER}/\${WIDTH}/\${HEIGHT}"></img></center>
+ <center><h2>Meow World!</h2></center>
+ Welcome to \${PREFIX}'s app. Replace this text with your own.
+ <!-- END -->
+
+ </div>
+ </body>
+</html>
+EOM
+
+echo "Script complete."
+
vault()
는 vault 연동시 사용가능 : https://www.packer.io/docs/templates/hcl_templates/functions/contextual/vault# packer init -upgrade .\\n# packer build -force .\\n\\nlocals {\\n client_id = vault(\\"/kv/data/azure\\", \\"client_id\\")\\n client_secret = vault(\\"/kv/data/azure\\", \\"client_secret\\")\\n tenant_id = vault(\\"/kv/data/azure\\", \\"tenant_id\\")\\n subscription_id = vault(\\"/kv/data/azure\\", \\"subscription_id\\")\\n resource_group_name = var.resource_name\\n virtual_network_name = \\"kbid-d-krc-vnet-002\\"\\n virtual_network_subnet_name = \\"d-mgmt-snet-001\\"\\n virtual_network_resource_group_name = \\"kbid-d-krc-mgmt-rg\\"\\n timestamp = formatdate(\\"YYYYMMDD_hhmmss\\", timeadd(timestamp(), \\"9h\\")) #생성되는 이미지 이름을 time 기반으로 생성\\n}\\n\\nvariable \\"placeholder\\" {\\n default = \\"placekitten.com\\"\\n description = \\"Image-as-a-service URL. Some other fun ones to try are fillmurray.com, placecage.com, placebeard.it, loremflickr.com, baconmockup.com, placeimg.com, placebear.com, placeskull.com, stevensegallery.com, placedog.net\\"\\n}\\n\\n# Basic example : https://www.packer.io/docs/builders/azure/arm#basic-example\\n# MS Guide : https://docs.microsoft.com/ko-kr/azure/virtual-machines/linux/build-image-with-packer\\nsource \\"azure-arm\\" \\"basic-example\\" {\\n client_id = local.client_id\\n client_secret = local.client_secret\\n subscription_id = local.subscription_id\\n tenant_id = local.tenant_id\\n\\n # shared_image_gallery {\\n # subscription = local.subscription_id\\n # resource_group = \\"myrg\\"\\n # gallery_name = \\"GalleryName\\"\\n # image_name = \\"gs_pkr_\${local.timestamp}\\"\\n # image_version = \\"1.0.0\\"\\n # }\\n managed_image_resource_group_name = local.resource_group_name\\n managed_image_name = \\"${var.image_name}-${local.timestamp}\\"\\n\\n os_type = \\"Linux\\"\\n # az vm image list-publishers --location koreacentral --output table\\n image_publisher = \\"RedHat\\"\\n # az vm image list-offers --location koreacentral --publisher RedHat --output table\\n image_offer = \\"RHEL\\"\\n # az vm image list-skus --location koreacentral --publisher RedHat --offer RHEL --output table\\n image_sku = \\"8_4\\"\\n\\n azure_tags = {\\n dept = \\"KBHC Terraform POC\\"\\n }\\n \\n # az vm list-skus --location koreacentral --all --output table\\n build_resource_group_name = local.resource_group_name\\n\\n #########################################\\n # 기존 생성되어있는 network 를 사용하기 위한 항목 #\\n #########################################\\n virtual_network_name = local.virtual_network_name\\n virtual_network_subnet_name = local.virtual_network_subnet_name\\n virtual_network_resource_group_name = local.virtual_network_resource_group_name\\n \\n # location = \\"koreacentral\\"\\n vm_size = \\"Standard_A2_v2\\"\\n}\\n\\nbuild {\\n sources = [\\"sources.azure-arm.basic-example\\"]\\n\\n provisioner \\"file\\" {\\n source = \\"./files/\\"\\n destination = \\"/tmp\\"\\n }\\n\\n# Vault OTP\\n provisioner \\"shell\\" {\\n inline = [\\n \\"sudo cp /tmp/sshd /etc/pam.d/sshd\\",\\n \\"sudo cp /tmp/sshd_config /etc/ssh/sshd_config\\",\\n \\"sudo mkdir -p /etc/vault.d\\",\\n \\"sudo cp /tmp/vault.hcl /etc/vault.d/vault.hcl\\",\\n \\"sudo cp /tmp/vault-ssh-helper /usr/bin/vault-ssh-helper\\",\\n \\"echo \\\\\\"=== Vault_Check ===\\\\\\"\\",\\n \\"curl http://10.0.9.10:8200\\",\\n \\"/usr/bin/vault-ssh-helper -verify-only -config=/etc/vault.d/vault.hcl -dev\\",\\n \\"echo \\\\\\"=== Add User ===\\\\\\"\\",\\n \\"sudo adduser jboss\\",\\n \\"echo password | sudo passwd --stdin jboss\\",\\n \\"echo 'jboss ALL=(ALL) NOPASSWD: ALL' | sudo tee -a /etc/sudoers\\",\\n \\"echo \\\\\\"=== SELINUX DISABLE ===\\\\\\"\\",\\n \\"sudo sed -ie 's/SELINUX=enforcing/SELINUX=disabled /g' /etc/selinux/config\\"\\n ]\\n }\\n\\n# Apache\\n provisioner \\"shell\\" {\\n inline = [\\n \\"sudo yum -y update\\",\\n \\"sleep 15\\",\\n \\"sudo yum -y update\\",\\n \\"sudo yum -y install httpd\\",\\n \\"sudo systemctl enable httpd\\",\\n \\"sudo systemctl start httpd\\",\\n \\"chmod +x /tmp/deploy_app.sh\\",\\n \\"sudo PLACEHOLDER=${var.placeholder} WIDTH=600 HEIGHT=800 PREFIX=gs /tmp/deploy_app.sh\\",\\n \\"sudo firewall-cmd --zone=public --permanent --add-port=80/tcp\\",\\n \\"sudo firewall-cmd --reload\\",\\n ]\\n }\\n}\\n
provider "boundary" {
+ addr = "http://172.28.128.11:9200"
+// recovery_kms_hcl = <<EOT
+// kms "aead" {
+// purpose = "recovery"
+// aead_type = "aes-gcm"
+// key = "8fZBjCUfN0TzjEGLQldGY4+iE9AkOvCfjh7+p0GtRBQ="
+// key_id = "global_recovery"
+// }
+// EOT
+ auth_method_id = "ampw_U6FXouWRDK"
+ password_auth_method_login_name = "admin"
+ password_auth_method_password = "POByMKtvabYS1wtRHLgZ"
+}
+
+resource "boundary_scope" "global" {
+ global_scope = true
+ scope_id = "global"
+ description = "Global scope"
+}
+
+// Scope HashiStack
+resource "boundary_scope" "corp" {
+ name = "hashistack"
+ description = "hashistack scope"
+ scope_id = boundary_scope.global.id
+ auto_create_admin_role = true
+ auto_create_default_role = true
+}
+
+resource "boundary_auth_method" "corp_password" {
+ name = "corp_password_auth_method"
+ description = "Password auth method"
+ type = "password"
+ scope_id = boundary_scope.corp.id
+}
+
+resource "boundary_account" "user" {
+ for_each = var.users
+ name = each.key
+ description = "User account for my user"
+ type = "password"
+ login_name = lower(each.key)
+ password = "password"
+ auth_method_id = boundary_auth_method.corp_password.id
+}
+
+resource "boundary_user" "users" {
+ for_each = var.users
+ name = each.key
+ description = "User resource for ${each.key}"
+ account_ids = ["${boundary_account.user[each.key].id}"]
+ scope_id = boundary_scope.corp.id
+}
+
+resource "boundary_group" "admin" {
+ name = "admin"
+ description = "Organization group for readonly users"
+ member_ids = [for user in boundary_user.users : user.id]
+ scope_id = boundary_scope.corp.id
+}
+
+resource "boundary_user" "readonly_users" {
+ for_each = var.readonly_users
+ name = each.key
+ description = "User resource for ${each.key}"
+ scope_id = boundary_scope.corp.id
+}
+
+resource "boundary_group" "readonly" {
+ name = "read-only"
+ description = "Organization group for readonly users"
+ member_ids = [for user in boundary_user.readonly_users : user.id]
+ scope_id = boundary_scope.corp.id
+}
+
+resource "boundary_role" "corp_admin" {
+ name = "corp_admin"
+ description = "Corp Administrator role"
+ principal_ids = concat(
+ [for user in boundary_user.users: user.id]
+ )
+ grant_strings = ["id=*;type=*;actions=create,read,update,delete"]
+ scope_id = boundary_scope.corp.id
+}
+
+resource "boundary_role" "organization_readonly" {
+ name = "Read-only"
+ description = "Read-only role"
+ principal_ids = [boundary_group.readonly.id]
+ grant_strings = ["id=*;type=*;actions=read"]
+ scope_id = boundary_scope.corp.id
+}
+
+resource "boundary_scope" "core_infra" {
+ name = "core_infra"
+ description = "My first project!"
+ scope_id = boundary_scope.corp.id
+ auto_create_admin_role = true
+}
+
+resource "boundary_host_catalog" "backend_servers" {
+ name = "backend_servers"
+ description = "Backend servers host catalog"
+ type = "static"
+ scope_id = boundary_scope.core_infra.id
+}
+
+resource "boundary_host" "ssh_servers" {
+ for_each = var.ssh_server_ips
+ type = "static"
+ name = "ssh_server_service_${each.value}"
+ description = "ssh server host"
+ address = each.key
+ host_catalog_id = boundary_host_catalog.backend_servers.id
+}
+
+resource "boundary_host" "backend_servers" {
+ for_each = var.backend_server_ips
+ type = "static"
+ name = "backend_server_service_${each.value}"
+ description = "Backend server host"
+ address = each.key
+ host_catalog_id = boundary_host_catalog.backend_servers.id
+}
+
+resource "boundary_host_set" "ssh_servers" {
+ type = "static"
+ name = "ssh_servers"
+ description = "Host set for ssh servers"
+ host_catalog_id = boundary_host_catalog.backend_servers.id
+ host_ids = [for host in boundary_host.ssh_servers : host.id]
+}
+
+resource "boundary_host_set" "backend_servers" {
+ type = "static"
+ name = "backend_servers"
+ description = "Host set for backend servers"
+ host_catalog_id = boundary_host_catalog.backend_servers.id
+ host_ids = [for host in boundary_host.backend_servers : host.id]
+}
+
+# create target for accessing backend servers on port :8000
+resource "boundary_target" "backend_servers_service" {
+ type = "tcp"
+ name = "backend_server"
+ description = "Backend service target"
+ scope_id = boundary_scope.core_infra.id
+ default_port = "8080"
+
+ host_set_ids = [
+ boundary_host_set.backend_servers .id
+ ]
+}
+
+# create target for accessing backend servers on port :22
+resource "boundary_target" "backend_servers_ssh" {
+ type = "tcp"
+ name = "ssh_server"
+ description = "Backend SSH target"
+ scope_id = boundary_scope.core_infra.id
+ // default_port = "22"
+
+ host_set_ids = [
+ boundary_host_set.ssh_servers.id
+ ]
+}
+
+// anonymous
+resource "boundary_role" "global_anon_listing" {
+ scope_id = boundary_scope.global.id
+ grant_strings = [
+ "id=*;type=auth-method;actions=list,authenticate",
+ "type=scope;actions=list",
+ "id={{account.id}};actions=read,change-password"
+ ]
+ principal_ids = ["u_anon"]
+}
+
+resource "boundary_role" "org_anon_listing" {
+ scope_id = boundary_scope.corp.id
+ grant_strings = [
+ "id=*;type=auth-method;actions=list,authenticate",
+ "type=scope;actions=list",
+ "id={{account.id}};actions=read,change-password"
+ ]
+ principal_ids = ["u_anon"]
+}
+
+output "corp_auth_method_id" {
+ value = "boundary authenticate password -auth-method-id ${boundary_auth_method.corp_password.id} -login-name ${boundary_account.user["gslee"].login_name} -password ${boundary_account.user["gslee"].password}"
+}
+
variable "addr" {
+ default = "http://172.28.128.11:9200"
+}
+
+variable "users" {
+ type = set(string)
+ default = [
+ "gslee",
+ "Jim",
+ "Mike",
+ "Todd",
+ "Jeff",
+ "Randy",
+ "Susmitha"
+ ]
+}
+
+variable "readonly_users" {
+ type = set(string)
+ default = [
+ "Chris",
+ "Pete",
+ "Justin"
+ ]
+}
+
+variable "ssh_server_ips" {
+ type = set(string)
+ default = [
+ "172.28.128.11"
+ ]
+}
+
+variable "backend_server_ips" {
+ type = set(string)
+ default = [
+ "172.28.128.11",
+ "172.28.128.50",
+ "172.28.128.60",
+ "172.28.128.61",
+ "172.28.128.70",
+ ]
+}
+
::: chart A bar chart
+
+\`\`\`json
+{
+ "type": "bar",
+ "data": {
+ "labels": ["Red", "Orange", "Yellow", "Green", "Blue", "Purple"],
+ "datasets": [{
+ "label": "My First Dataset",
+ "data": [12, 19, 3, 5, 2, 3],
+ "backgroundColor": [
+ "rgba(255, 99, 132, 0.2)",
+ "rgba(255, 159, 64, 0.2)",
+ "rgba(255, 205, 86, 0.2)",
+ "rgba(75, 192, 192, 0.2)",
+ "rgba(54, 162, 235, 0.2)",
+ "rgba(153, 102, 255, 0.2)"
+ ],
+ "borderColor": [
+ "rgb(255, 99, 132)",
+ "rgb(255, 159, 64)",
+ "rgb(255, 205, 86)",
+ "rgb(75, 192, 192)",
+ "rgb(54, 162, 235)",
+ "rgb(153, 102, 255)"
+ ],
+ "borderWidth": 1
+ }]
+ },
+ "options": {
+ "scales": {
+ "y": {
+ "ticks": {
+ "beginAtZero": true,
+ "callback": "function(value){ return '$' + value + 'k'; }"
+ },
+ "beginAtZero": true
+ }
+ }
+ }
+}
+\`\`\`
+
+
+
::: chart A Bubble Chart
+
+\`\`\`json
+{
+ "type": "bubble",
+ "data": {
+ "datasets": [
+ {
+ "label": "First Dataset",
+ "data": [
+ { "x": 20, "y": 30, "r": 15 },
+ { "x": 40, "y": 10, "r": 10 }
+ ],
+ "backgroundColor": "rgb(255, 99, 132)"
+ }
+ ]
+ }
+}
+\`\`\`
+
+
문서 작성시 차트를 추가하는 방법을 안내합니다.
\\n차트 구성 방식은 ChartJS를 따릅니다.
\\n::: chart
와 :::
로 처리합니다.
팁
Nomad에서 docker 자체의 로깅을 사용하므로서, Nomad에서 실행되는 docker 기반 컨테이너의 로깅이 특정 환경에 락인되는것을 방지합니다.
경고
AWS 환경이 아닌 외부 구성 시, 해당 노드에 Cloudwath 기록을 위한 Policy를 갖는 IAM의 credential 정보가 환경변수 또는 ~/.aws/credential
구성이 필요합니다.
Nomad 구성 시 Cloudwatch에 대한 EC2 Instance의 IAM 구성이 필요합니다. 아래 Terraform 구성의 예를 참고하세요.
',4),b=n("li",null,[s("loging driver의 구성에 따라 "),n("code",null,"aws_iam_role_policy"),s("에 설정하는 필요한 권한에 차이가 있을 수 있습니다.")],-1),h=n("li",null,[s("예를 들어 docker loging 구성에서 "),n("code",null,"awslogs-create-group = true"),s(" 옵션을 추가하려는 경우 "),n("code",null,"logs:CreateLogGroup"),s(" 정책이 필요합니다.")],-1),v={href:"https://docs.aws.amazon.com/ko_kr/AmazonCloudWatch/latest/logs/permissions-reference-cwl.html",target:"_blank",rel:"noopener noreferrer"},_=n("div",{class:"language-hcl line-numbers-mode","data-ext":"hcl","data-title":"hcl"},[n("pre",{hcl:"",class:"language-hcl"},[n("code",null,[n("span",{class:"token comment"},"## 생략 ##"),s(` + +`),n("span",{class:"token keyword"},[s("resource "),n("span",{class:"token type variable"},'"aws_iam_instance_profile"')]),s(),n("span",{class:"token string"},'"ec2_profile"'),s(),n("span",{class:"token punctuation"},"{"),s(` + `),n("span",{class:"token property"},"name"),s(),n("span",{class:"token punctuation"},"="),s(),n("span",{class:"token string"},'"ec2_profile"'),s(` + `),n("span",{class:"token property"},"role"),s(),n("span",{class:"token punctuation"},"="),s(` aws_iam_role.role.name +`),n("span",{class:"token punctuation"},"}"),s(` + +`),n("span",{class:"token keyword"},[s("resource "),n("span",{class:"token type variable"},'"aws_iam_role"')]),s(),n("span",{class:"token string"},'"role"'),s(),n("span",{class:"token punctuation"},"{"),s(` + `),n("span",{class:"token property"},"name"),s(),n("span",{class:"token punctuation"},"="),s(),n("span",{class:"token string"},'"my_role"'),s(` + + `),n("span",{class:"token property"},"assume_role_policy"),s(),n("span",{class:"token punctuation"},"="),s(" jsonencode("),n("span",{class:"token punctuation"},"{"),s(` + `),n("span",{class:"token property"},"Version"),s(),n("span",{class:"token punctuation"},"="),s(),n("span",{class:"token string"},'"2012-10-17"'),s(` + `),n("span",{class:"token property"},"Statement"),s(),n("span",{class:"token punctuation"},"="),s(),n("span",{class:"token punctuation"},"["),s(` + `),n("span",{class:"token punctuation"},"{"),s(` + `),n("span",{class:"token property"},"Action"),s(),n("span",{class:"token punctuation"},"="),s(),n("span",{class:"token string"},'"sts:AssumeRole"'),s(` + `),n("span",{class:"token property"},"Effect"),s(),n("span",{class:"token punctuation"},"="),s(),n("span",{class:"token string"},'"Allow"'),s(` + `),n("span",{class:"token property"},"Sid"),s(" "),n("span",{class:"token punctuation"},"="),s(),n("span",{class:"token string"},'""'),s(` + `),n("span",{class:"token property"},"Principal"),s(),n("span",{class:"token punctuation"},"="),s(),n("span",{class:"token punctuation"},"{"),s(` + `),n("span",{class:"token property"},"Service"),s(),n("span",{class:"token punctuation"},"="),s(),n("span",{class:"token string"},'"ec2.amazonaws.com"'),s(` + `),n("span",{class:"token punctuation"},"}"),s(` + `),n("span",{class:"token punctuation"},"}"),s(`, + `),n("span",{class:"token punctuation"},"]"),s(` + `),n("span",{class:"token punctuation"},"}"),s(`) +`),n("span",{class:"token punctuation"},"}"),s(` + +`),n("span",{class:"token keyword"},[s("resource "),n("span",{class:"token type variable"},'"aws_iam_role_policy"')]),s(),n("span",{class:"token string"},'"cloudwatch_policy"'),s(),n("span",{class:"token punctuation"},"{"),s(` + `),n("span",{class:"token property"},"name"),s(" "),n("span",{class:"token punctuation"},"="),s(),n("span",{class:"token string"},'"cloudwatch_policy"'),s(` + `),n("span",{class:"token property"},"role"),s(" "),n("span",{class:"token punctuation"},"="),s(` aws_iam_role.role.id + + `),n("span",{class:"token property"},"policy"),s(),n("span",{class:"token punctuation"},"="),s(" jsonencode("),n("span",{class:"token punctuation"},"{"),s(` + `),n("span",{class:"token property"},"Version"),s(),n("span",{class:"token punctuation"},"="),s(),n("span",{class:"token string"},'"2012-10-17"'),s(` + `),n("span",{class:"token property"},"Statement"),s(),n("span",{class:"token punctuation"},"="),s(),n("span",{class:"token punctuation"},"["),s(` + `),n("span",{class:"token punctuation"},"{"),s(` + `),n("span",{class:"token property"},"Action"),s(),n("span",{class:"token punctuation"},"="),s(),n("span",{class:"token punctuation"},"["),s(` + `),n("span",{class:"token string"},'"logs:CreateLogStream"'),s(`, + `),n("span",{class:"token string"},'"logs:PutLogEvents"'),s(`, + `),n("span",{class:"token string"},'"logs:CreateLogGroup"'),s(` + `),n("span",{class:"token punctuation"},"]"),s(` + `),n("span",{class:"token property"},"Effect"),s(" "),n("span",{class:"token punctuation"},"="),s(),n("span",{class:"token string"},'"Allow"'),s(` + `),n("span",{class:"token property"},"Resource"),s(),n("span",{class:"token punctuation"},"="),s(),n("span",{class:"token string"},'"*"'),s(` + `),n("span",{class:"token punctuation"},"}"),s(`, + `),n("span",{class:"token punctuation"},"]"),s(` + `),n("span",{class:"token punctuation"},"}"),s(`) +`),n("span",{class:"token punctuation"},"}"),s(` + +`),n("span",{class:"token keyword"},[s("resource "),n("span",{class:"token type variable"},'"aws_instance"')]),s(),n("span",{class:"token string"},'"example"'),s(),n("span",{class:"token punctuation"},"{"),s(` + `),n("span",{class:"token property"},"ami"),s(" "),n("span",{class:"token punctuation"},"="),s(),n("span",{class:"token string"},'"ami-04e6fcf8cfe3b09ea"'),s(` + `),n("span",{class:"token property"},"instance_type"),s(),n("span",{class:"token punctuation"},"="),s(),n("span",{class:"token string"},'"t2.micro"'),s(` + `),n("span",{class:"token property"},"key_name"),s(" "),n("span",{class:"token punctuation"},"="),s(` aws_key_pair.web_admin.key_name + `),n("span",{class:"token property"},"vpc_security_group_ids"),s(),n("span",{class:"token punctuation"},"="),s(),n("span",{class:"token punctuation"},"["),s(` + aws_security_group.ssh.id + `),n("span",{class:"token punctuation"},"]"),s(` + + `),n("span",{class:"token property"},"iam_instance_profile"),s(),n("span",{class:"token punctuation"},"="),s(` aws_iam_instance_profile.ec2_profile.name +`),n("span",{class:"token punctuation"},"}"),s(` +`)])]),n("div",{class:"highlight-lines"},[n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("div",{class:"highlight-line"}," "),n("div",{class:"highlight-line"}," "),n("div",{class:"highlight-line"}," "),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("div",{class:"highlight-line"}," "),n("br")]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),y=n("h2",{id:"nomad-job의-docker-driver에-logging-설정",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#nomad-job의-docker-driver에-logging-설정"},[n("span",null,"Nomad Job의 Docker Driver에 Logging 설정")])],-1),w=n("code",null,"json-file",-1),f={href:"https://www.nomadproject.io/docs/drivers/docker#logging",target:"_blank",rel:"noopener noreferrer"},C={href:"https://docs.docker.com/config/containers/logging/configure/",target:"_blank",rel:"noopener noreferrer"},N=n("li",null,[s("기존 docker cli 상에 구성했던 "),n("code",null,"--log-driver"),s(" 같은 옵션의 정의가 HCL형태로 정의됩니다.")],-1),L=n("li",null,[s("HCL 문법을 따르므로, 몇몇 상이한 표현방식이 있을 수 있습니다. 예를들어 로그 날짜 구성에 사용되는 "),n("code",null,'"\\[%Y-%m-%d\\]"'),s(" 에서 "),n("code",null,"["),s(" 같은 특수문자 표기를 위해 "),n("code",null,"\\"),s("를 한번만 넣었다면, "),n("code",null,'"\\\\[%Y-%m-%d\\\\]"'),s(" 같이 두번 넣어야 할수도 있습니다.")],-1),x=n("p",null,"구성 예제는 아래와 같습니다.",-1),A=n("div",{class:"language-hcl line-numbers-mode","data-ext":"hcl","data-title":"hcl"},[n("pre",{hcl:"",class:"language-hcl"},[n("code",null,[s("job "),n("span",{class:"token string"},'"api"'),s(),n("span",{class:"token punctuation"},"{"),s(` + `),n("span",{class:"token property"},"datacenters"),s(),n("span",{class:"token punctuation"},"="),s(),n("span",{class:"token punctuation"},"["),n("span",{class:"token string"},'"dc1"'),n("span",{class:"token punctuation"},"]"),s(` + `),n("span",{class:"token property"},"type"),s(),n("span",{class:"token punctuation"},"="),s(),n("span",{class:"token string"},'"service"'),s(` + + group `),n("span",{class:"token string"},'"api"'),s(),n("span",{class:"token punctuation"},"{"),s(` + `),n("span",{class:"token keyword"},"network"),s(),n("span",{class:"token punctuation"},"{"),s(` + `),n("span",{class:"token property"},"mode"),s(),n("span",{class:"token punctuation"},"="),s(),n("span",{class:"token string"},'"bridge"'),s(` + port `),n("span",{class:"token string"},'"api"'),s(),n("span",{class:"token punctuation"},"{"),s(` + `),n("span",{class:"token property"},"to"),s(),n("span",{class:"token punctuation"},"="),s(),n("span",{class:"token number"},"9001"),s(` + `),n("span",{class:"token punctuation"},"}"),s(` + `),n("span",{class:"token punctuation"},"}"),s(` + + `),n("span",{class:"token keyword"},"service"),s(),n("span",{class:"token punctuation"},"{"),s(` + `),n("span",{class:"token property"},"name"),s(),n("span",{class:"token punctuation"},"="),s(),n("span",{class:"token string"},'"count-api"'),s(` + `),n("span",{class:"token property"},"port"),s(),n("span",{class:"token punctuation"},"="),s(),n("span",{class:"token string"},'"api"'),s(` + `),n("span",{class:"token keyword"},"connect"),s(),n("span",{class:"token punctuation"},"{"),s(` + `),n("span",{class:"token keyword"},"sidecar_service"),s(),n("span",{class:"token punctuation"},"{"),n("span",{class:"token punctuation"},"}"),s(` + `),n("span",{class:"token punctuation"},"}"),s(` + `),n("span",{class:"token punctuation"},"}"),s(` + + task `),n("span",{class:"token string"},'"web"'),s(),n("span",{class:"token punctuation"},"{"),s(` + `),n("span",{class:"token property"},"driver"),s(),n("span",{class:"token punctuation"},"="),s(),n("span",{class:"token string"},'"docker"'),s(` + `),n("span",{class:"token keyword"},"config"),s(),n("span",{class:"token punctuation"},"{"),s(` + `),n("span",{class:"token property"},"image"),s(),n("span",{class:"token punctuation"},"="),s(),n("span",{class:"token string"},'"hashicorpnomad/counter-api:v1"'),s(` + `),n("span",{class:"token property"},"ports"),s(),n("span",{class:"token punctuation"},"="),s(),n("span",{class:"token punctuation"},"["),n("span",{class:"token string"},'"api"'),n("span",{class:"token punctuation"},"]"),s(` + `),n("span",{class:"token keyword"},"logging"),s(),n("span",{class:"token punctuation"},"{"),s(` + `),n("span",{class:"token property"},"type"),s(),n("span",{class:"token punctuation"},"="),s(),n("span",{class:"token string"},'"awslogs"'),s(` + `),n("span",{class:"token keyword"},"config"),s(),n("span",{class:"token punctuation"},"{"),s(` + `),n("span",{class:"token property"},"awslogs-region"),s(),n("span",{class:"token punctuation"},"="),s(),n("span",{class:"token string"},'"ap-northeast-2"'),s(` + `),n("span",{class:"token property"},"awslogs-group"),s(),n("span",{class:"token punctuation"},"="),s(),n("span",{class:"token string"},'"myGroup"'),s(` + `),n("span",{class:"token property"},"awslogs-create-group"),s(),n("span",{class:"token punctuation"},"="),s(),n("span",{class:"token boolean"},"true"),s(` + `),n("span",{class:"token property"},"awslogs-datetime-format"),s(),n("span",{class:"token punctuation"},"="),s(),n("span",{class:"token string"},'"\\\\[%Y-%m-%dT%H:%M:%S\\\\+09:00\\\\]"'),s(` + `),n("span",{class:"token punctuation"},"}"),s(` + `),n("span",{class:"token punctuation"},"}"),s(` + `),n("span",{class:"token punctuation"},"}"),s(` + `),n("span",{class:"token punctuation"},"}"),s(` + `),n("span",{class:"token punctuation"},"}"),s(` +`),n("span",{class:"token punctuation"},"}"),s(` +`)])]),n("div",{class:"highlight-lines"},[n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("br"),n("div",{class:"highlight-line"}," "),n("div",{class:"highlight-line"}," "),n("div",{class:"highlight-line"}," "),n("div",{class:"highlight-line"}," "),n("div",{class:"highlight-line"}," "),n("div",{class:"highlight-line"}," "),n("div",{class:"highlight-line"}," "),n("div",{class:"highlight-line"}," "),n("div",{class:"highlight-line"}," "),n("br"),n("br"),n("br"),n("br")]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),D=n("h2",{id:"log-확인",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#log-확인"},[n("span",null,"Log 확인")])],-1),j=n("p",null,[s("Nomad의 로그 출력을 확인합니다."),n("br"),n("img",{src:r,alt:"NomadLog",loading:"lazy"})],-1),S=n("p",null,[s("Cloudwatch에 로그 출력을 확인합니다."),n("br"),n("img",{src:p,alt:"NomadLog",loading:"lazy"})],-1);function E(T,z){const e=o("ExternalLinkIcon");return l(),c("div",null,[d,n("p",null,[s('docker 런타임에는 log driver로 "awslogs"를 지원합니다.'),k,n("a",g,[s("https://docs.docker.com/config/containers/logging/awslogs/"),a(e)])]),m,n("ul",null,[b,h,n("li",null,[s("권한에 대한 상세 설명은 다음 링크를 참고합니다. "),n("a",v,[s("https://docs.aws.amazon.com/ko_kr/AmazonCloudWatch/latest/logs/permissions-reference-cwl.html"),a(e)])])]),_,y,n("p",null,[s("Nomad에서 docker 드라이버 사용시 적용되는 기본 log driver는 "),w,s(" 입니다. 추가 설정을 통해 docker가 지원하는 다양한 log driver를 사용할 수 있습니다. ("),n("a",f,[s("FluentD 샘플"),a(e)]),s(")")]),n("ul",null,[n("li",null,[s("구성에 필요한 정보는 Docker의 공식 문서를 참고 합니다. : "),n("a",C,[s("https://docs.docker.com/config/containers/logging/configure/"),a(e)])]),N,L]),x,A,D,j,S])}const H=t(u,[["render",E],["__file","Cloudwatch-Logging.html.vue"]]),V=JSON.parse('{"path":"/04-HashiCorp/07-Nomad/02-Config/Cloudwatch-Logging.html","title":"Docker log driver and Cloudwatch on Nomad","lang":"ko-KR","frontmatter":{"description":"Nomad docker job logging into Cloudwatch","tag":["Nomad","AWS","Cloudwatch","log"],"head":[["meta",{"property":"og:url","content":"https://docmoa.github.io/04-HashiCorp/07-Nomad/02-Config/Cloudwatch-Logging.html"}],["meta",{"property":"og:site_name","content":"docmoa"}],["meta",{"property":"og:title","content":"Docker log driver and Cloudwatch on Nomad"}],["meta",{"property":"og:description","content":"Nomad docker job logging into Cloudwatch"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"ko-KR"}],["meta",{"property":"og:updated_time","content":"2023-09-18T13:12:54.000Z"}],["meta",{"property":"article:tag","content":"Nomad"}],["meta",{"property":"article:tag","content":"AWS"}],["meta",{"property":"article:tag","content":"Cloudwatch"}],["meta",{"property":"article:tag","content":"log"}],["meta",{"property":"article:modified_time","content":"2023-09-18T13:12:54.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Docker log driver and Cloudwatch on Nomad\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2023-09-18T13:12:54.000Z\\",\\"author\\":[]}"]]},"headers":[{"level":2,"title":"EC2 Instance Role 구성","slug":"ec2-instance-role-구성","link":"#ec2-instance-role-구성","children":[]},{"level":2,"title":"Nomad Job의 Docker Driver에 Logging 설정","slug":"nomad-job의-docker-driver에-logging-설정","link":"#nomad-job의-docker-driver에-logging-설정","children":[]},{"level":2,"title":"Log 확인","slug":"log-확인","link":"#log-확인","children":[]}],"git":{"createdTime":1639533195000,"updatedTime":1695042774000,"contributors":[{"name":"Administrator","email":"admin@example.com","commits":3},{"name":"Great-Stone","email":"hahohh@gmail.com","commits":1}]},"readingTime":{"minutes":0.85,"words":256},"filePathRelative":"04-HashiCorp/07-Nomad/02-Config/Cloudwatch-Logging.md","localizedDate":"2021년 12월 15일","excerpt":"\\ndocker 런타임에는 log driver로 \\"awslogs\\"를 지원합니다.
\\nhttps://docs.docker.com/config/containers/logging/awslogs/
팁
\\nNomad에서 docker 자체의 로깅을 사용하므로서, Nomad에서 실행되는 docker 기반 컨테이너의 로깅이 특정 환경에 락인되는것을 방지합니다.
\\n마크다운 기본 사용 법과 거의 동일합니다.
코드블록은 ``` 과 ``` 사이에 코드를 넣어 로 표기합니다. 아래와 같이 md 파일 내에 작성하면
```\n# Code block e.g.\nThis is my code\n```\n
다음과 같이 표기됩니다.
# Code block e.g.\nThis is my code\n
```
대신 ~~~
도 사용 가능합니다.
```python\nprint("hello, world.")\n```\n
print("hello, world.")\n
문서 작성 시 코드블록에서 강조하고 싶은 경우 코드블럭의 스타일 에 강조할 라인 번호를 명시합니다.
```python {2,4-5}\nprint("nomal")\nprint("highlight!")\nprint("nomal")\nprint("highlight!")\nprint("highlight!")\n```\n
상황에 따라 동일한 구성 및 동작을 위한 코드블록을 옵션을 주어 선택적으로 표기하고 싶은 경우 <code-group>
과 <code-block>
을 활용 합니다.
::: code-tabs#shell
+@tab Option1
+\`\`\`bash {2}
+# This is Option 1
+chmod 755 ./file.txt
+\`\`\`
+
+@tab Option2
+\`\`\`bash {2}
+# This is Option 2
+chmod +x ./file.txt
+\`\`\`
+:::
+
::: normal-demo Demo
+
+\`\`\`html
+<h1>Mr.Hope</h1>
+<p>is <span id="very">very</span> handsome</p>
+\`\`\`
+
+\`\`\`js
+document.querySelector("#very").addEventListener("click", () => {
+ alert("Very handsome");
+});
+\`\`\`
+
+\`\`\`css
+span {
+ color: red;
+}
+\`\`\`
+
+:::
+
마크다운 기본 사용 법과 거의 동일합니다.
\\n코드블록은 ``` 과 ``` 사이에 코드를 넣어 로 표기합니다. 아래와 같이 md 파일 내에 작성하면
\\n```\\n# Code block e.g.\\nThis is my code\\n```\\n
하나의 Consul 서버에서 서로다른 Agent 들의 묶음을 관리할 수 있도록 하는 개념입니다. 여러개의 Consul 클러스터를 만드는 것이 아닌, 하나의 클러스터 내에서 Agent 들을 분류합니다.
',5),p={href:"https://learn.hashicorp.com/tutorials/consul/network-partition-datacenters",target:"_blank",rel:"noopener noreferrer"},h=e("h3",{id:"_2-advanced-federation",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#_2-advanced-federation"},[e("span",null,"2. Advanced Federation")])],-1),u=e("p",null,"페더레이션을 위해서는 RPC(8300/tcp), SerfWAN(8302/tcp, 8302/udp)를 통해야하므로 여러 테이터센터, 혹은 클러스터를 관리하기에 어려워집니다. 이를 보완하기 위해 지역간 트래픽을 모두 RPC(8300/tcp)를 통해 수행하여 TLS만으로 보안을 유지하도록 합니다.",-1),_={href:"https://www.consul.io/docs/enterprise/federation",target:"_blank",rel:"noopener noreferrer"},g=e("h3",{id:"_3-redendancy-zones",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#_3-redendancy-zones"},[e("span",null,"3. Redendancy Zones")])],-1),m=e("p",null,'일반적으로 클러스터의 관리 서버는 3중화하여 구성하며 Raft 알고리즘을 통해 리더 선출을 하는 방식을 취합니다. HA를 위해 해당 투표에 참여하지 않은 추가 잉여 서버 노드를 "상시 대기" 시킬 수 있으며 장애 발생 시 해당 노드는 투표 구성원으로 승격 됩니다. 이는 서버 노드에 대한 추가 구성원임과 동시에 복구 기능을 제공합니다.',-1),w={href:"https://learn.hashicorp.com/tutorials/consul/autopilot-datacenter-operations#redundancy-zones",target:"_blank",rel:"noopener noreferrer"},f=e("h3",{id:"_4-enganced-read-scalability",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#_4-enganced-read-scalability"},[e("span",null,"4. Enganced Read Scalability")])],-1),b=e("p",null,"앞서의 Redendancy Zone의 비투표 노드는 투표에는 참여하지 않지만 데이터를 복제하고 데이터를 읽을 수 있는 동작을 지원합니다. 이를 통해 Consul 클러스터를 확장할 수 있고 읽기/쓰기 지연시간을 줄이고 용량을 늘일 수 있습니다.",-1),y={href:"https://www.consul.io/docs/agent/options#_non_voting_server",target:"_blank",rel:"noopener noreferrer"},v=e("h2",{id:"governance-policy",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#governance-policy"},[e("span",null,"Governance & Policy")])],-1),k={href:"https://www.hashicorp.com/blog/enterprise-compliance-and-governance-with-hashicorp-consul-1-8/",target:"_blank",rel:"noopener noreferrer"},S=e("h3",{id:"_1-namespaces",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#_1-namespaces"},[e("span",null,"1. Namespaces")])],-1),C=e("p",null,"Namespaces 기능은 사용자나 팀간의 데이터 격리를 제공합니다. 각각의 Namespace로 구분된 서비스와 정보는 서로 다른 구성원같에 조회가 불가능합니다. 하나의 클러스터로 논리적 분할을 가능하게 합니다.",-1),E={href:"https://www.consul.io/docs/enterprise/namespaces",target:"_blank",rel:"noopener noreferrer"},x=e("h3",{id:"_2-single-sign-on",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#_2-single-sign-on"},[e("span",null,"2. Single Sign On")])],-1),N=e("p",null,"Open ID Connect를 사용하여 인증을 처리합니다.",-1),F={href:"https://www.consul.io/docs/acl/auth-methods/oidc",target:"_blank",rel:"noopener noreferrer"},R=e("h3",{id:"_3-audit-logging",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#_3-audit-logging"},[e("span",null,"3. Audit Logging")])],-1),A=e("p",null,"사용자의 이벤트 시도와 처리에 대한 로그를 캡쳐하고 기록하는 것을 지원합니다. 1.8.0 기준으로 'file'을 지원하며, 향후 추가 타입이 지원될 예정입니다.",-1),G={href:"https://www.consul.io/docs/agent/options#audit",target:"_blank",rel:"noopener noreferrer"},V=e("h3",{id:"_4-sentinel",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#_4-sentinel"},[e("span",null,"4. Sentinel")])],-1),T=e("p",null,"Consul에 반영되는 서비스나 KV에 대한 정책을 정의할 수 있습니다. 예를 들면 서비스 등록이나 업데이트에 대한 시간을 강제하거나 Key에 이름 패턴을 강제화 할 수 있습니다.",-1),U={href:"https://www.consul.io/docs/agent/sentinel",target:"_blank",rel:"noopener noreferrer"};function Z(z,L){const t=o("ExternalLinkIcon");return i(),r("div",null,[d,e("p",null,[n("참고 Url : "),e("a",p,[n("https://learn.hashicorp.com/tutorials/consul/network-partition-datacenters"),l(t)])]),h,u,e("p",null,[n("참고 Url : "),e("a",_,[n("https://www.consul.io/docs/enterprise/federation"),l(t)])]),g,m,e("p",null,[n("참고 Url : "),e("a",w,[n("https://learn.hashicorp.com/tutorials/consul/autopilot-datacenter-operations#redundancy-zones"),l(t)])]),f,b,e("p",null,[n("참고 Url : "),e("a",y,[n("https://www.consul.io/docs/agent/options#_non_voting_server"),l(t)])]),v,e("p",null,[n("관련 블로그 : "),e("a",k,[n("https://www.hashicorp.com/blog/enterprise-compliance-and-governance-with-hashicorp-consul-1-8/"),l(t)])]),S,C,e("p",null,[n("참고 Url : "),e("a",E,[n("https://www.consul.io/docs/enterprise/namespaces"),l(t)])]),x,N,e("p",null,[n("참고 Url : "),e("a",F,[n("https://www.consul.io/docs/acl/auth-methods/oidc"),l(t)])]),R,A,e("p",null,[n("참고 Url : "),e("a",G,[n("https://www.consul.io/docs/agent/options#audit"),l(t)])]),V,T,e("p",null,[n("참고 Url : "),e("a",U,[n("https://www.consul.io/docs/agent/sentinel"),l(t)])])])}const O=a(c,[["render",Z],["__file","Consul Enterprise Feature.html.vue"]]),H=JSON.parse('{"path":"/04-HashiCorp/04-Consul/01-Information/Consul%20Enterprise%20Feature.html","title":"Consul Enterprise Feature","lang":"ko-KR","frontmatter":{"description":"Consul Enterprise Feature","tag":["Consul","Enterprise"],"head":[["meta",{"property":"og:url","content":"https://docmoa.github.io/04-HashiCorp/04-Consul/01-Information/Consul%20Enterprise%20Feature.html"}],["meta",{"property":"og:site_name","content":"docmoa"}],["meta",{"property":"og:title","content":"Consul Enterprise Feature"}],["meta",{"property":"og:description","content":"Consul Enterprise Feature"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"ko-KR"}],["meta",{"property":"og:updated_time","content":"2023-09-18T13:12:54.000Z"}],["meta",{"property":"article:tag","content":"Consul"}],["meta",{"property":"article:tag","content":"Enterprise"}],["meta",{"property":"article:modified_time","content":"2023-09-18T13:12:54.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Consul Enterprise Feature\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2023-09-18T13:12:54.000Z\\",\\"author\\":[]}"]]},"headers":[{"level":2,"title":"Enterprise Global Visibility & Scale","slug":"enterprise-global-visibility-scale","link":"#enterprise-global-visibility-scale","children":[{"level":3,"title":"1. Network Segments","slug":"_1-network-segments","link":"#_1-network-segments","children":[]},{"level":3,"title":"2. Advanced Federation","slug":"_2-advanced-federation","link":"#_2-advanced-federation","children":[]},{"level":3,"title":"3. Redendancy Zones","slug":"_3-redendancy-zones","link":"#_3-redendancy-zones","children":[]},{"level":3,"title":"4. Enganced Read Scalability","slug":"_4-enganced-read-scalability","link":"#_4-enganced-read-scalability","children":[]}]},{"level":2,"title":"Governance & Policy","slug":"governance-policy","link":"#governance-policy","children":[{"level":3,"title":"1. Namespaces","slug":"_1-namespaces","link":"#_1-namespaces","children":[]},{"level":3,"title":"2. Single Sign On","slug":"_2-single-sign-on","link":"#_2-single-sign-on","children":[]},{"level":3,"title":"3. Audit Logging","slug":"_3-audit-logging","link":"#_3-audit-logging","children":[]},{"level":3,"title":"4. Sentinel","slug":"_4-sentinel","link":"#_4-sentinel","children":[]}]}],"git":{"createdTime":1628557352000,"updatedTime":1695042774000,"contributors":[{"name":"Great-Stone","email":"hahohh@gmail.com","commits":2}]},"readingTime":{"minutes":0.5,"words":151},"filePathRelative":"04-HashiCorp/04-Consul/01-Information/Consul Enterprise Feature.md","localizedDate":"2021년 8월 10일","excerpt":"\\nconsul monitor -log-level=debug
+
==정상적인 경우
2020/10/19 16:21:23 [INFO] raft: Node at 10.90.168.42:8300 [Candidate] entering Candidate state in term 3732
+2020/10/19 16:21:23 [DEBUG] raft: Votes needed: 2
+2020/10/19 16:21:23 [DEBUG] raft: Vote granted from foobar in term 3732. Tally: 1
+
== 비 정상적인 경우
2020/10/19 16:28:53 [WARN] raft: Election timeout reached, restarting election
+2020/10/19 16:28:53 [INFO] raft: Node at 00.00.000.00:8300 [Candidate] entering Candidate state in term 992
+2020/10/19 16:28:53 [DEBUG] raft: Votes needed: 2
+2020/10/19 16:28:53 [DEBUG] raft: Vote granted from foobar2 in term 992. Tally: 1
+2020/10/19 16:28:53 [ERR] raft: Failed to make RequestVote RPC to {Voter <Voter ID>)
+
복구 시 정상화된 로그
+020/10/19 16:29:04 [WARN] raft: Election timeout reached, restarting election
+2020/10/19 16:29:04 [INFO] raft: Node at 00.00.000.00:8300 [Candidate] entering Candidate state in term 989
+2020/10/19 16:29:04 [DEBUG] raft: Votes needed: 2
+2020/10/19 16:29:04 [DEBUG] raft: Vote granted from <ID> in term 989. Tally: 1
+
\\n\\n"}');export{_ as comp,C as data}; diff --git a/assets/Consul Enterprise Feature.html-I88Lp2Lj.js b/assets/Consul Enterprise Feature.html-I88Lp2Lj.js new file mode 100644 index 0000000000..f24553071b --- /dev/null +++ b/assets/Consul Enterprise Feature.html-I88Lp2Lj.js @@ -0,0 +1,385 @@ +import{_ as l}from"./plugin-vue_export-helper-DlAUqK2U.js";import{r as o,o as i,c,b as n,d as s,a as e,e as t}from"./app-Bzk8Nrll.js";const p={},r=t('
이 문서에서는 Consul을 사용하여 상이한 두 Consul로 구성된 클러스터(마스터가 별개)의 서비스를 연계하는 방법을 설명합니다.
네트워크 영역이 분리되어있는 두 환경의 애플리케이션 서비스들을 Service Mesh로 구성하는 방법을 알아 봅니다. 이번 구성 예에서는 Kubernetes와 Baremetal(BM)이나 VirtualMachine(VM)에 Consul Cluster(Datacenter)를 구성하고 각 환경의 애플리케이션 서비스를 Mesh Gateway로 연계합니다.
Mesh Gateway를 사용하면 서로다른 클러스터간에 mTLS 환경의 통신과 서비스 간의 트래픽 통로를 단일화 하여 구성할 수 있습니다. 또한 mTLS내의 데이터가 Gateway에서 해동되지 않기 때문에 두 클러스터간 안전하게 데이터를 송수신 합니다.
Consul의 각 Cluster는 Datacenter라는 명칭으로 구분됩니다. 이번 구성에서는 Kubernetes의 Consul Datacenter가 Primary의 역할을 합니다.
Use | Default Ports | CLI |
---|---|---|
DNS: The DNS server (TCP and UDP) | 8600 | -dns-port |
HTTP: The HTTP API (TCP Only) | 8500 | -http-port |
HTTPS: The HTTPs API | disabled (8501)* | -https-port |
gRPC: The gRPC API | disabled (8502)* | -grpc-port |
LAN Serf: The Serf LAN port (TCP and UDP) | 8301 | -serf-lan-port |
Wan Serf: The Serf WAN port (TCP and UDP) | 8302 | -sert-wan-port |
server: Server RPC address (TCP Only) | 8300 | -server-port |
Sidecar Proxy Min: Sidecar 서비스 등록에 사용되는 범위의 최소 포트 | 21000 | Configration file |
Sidecar Proxy Max: Sidecar 서비스 등록에 사용되는 범위의 최대 포트 | 21255 | Configration file |
Federation을 위한 포트로는
ports {
+ dns = 8600
+ http = 8500
+ https = -1
+ grpc = -1
+ serf_lan = 8301
+ serf_wan = 8302
+ server = 8300
+ sidecar_min_port = 21000
+ sidecar_max_port = 21255
+ expose_min_port = 21500
+ expose_max_port = 21755
+}
+
HashiCorp helm Repository를 추가합니다.
$ helm repo add hashicorp https://helm.releases.hashicorp.com
+"hashicorp" has been added to your repositories
+
Consul 차트에 접근가능한지 확인합니다.
$ helm search repo hashicorp/consul
+NAME CHART VERSION APP VERSION DESCRIPTION
+hashicorp/consul 0.32.1 1.10.0 Official HashiCorp Consul Chart
+
Consul 차트마다의 기본 매칭되는 버전정보는 다음과 같이 리스트로 확인 가능합니다.
$ helm search repo hashicorp/consul -l
+NAME CHART VERSION APP VERSION DESCRIPTION
+hashicorp/consul 0.32.1 1.10.0 Official HashiCorp Consul Chart
+hashicorp/consul 0.32.0 1.10.0 Official HashiCorp Consul Chart
+hashicorp/consul 0.31.1 1.9.4 Official HashiCorp Consul Chart
+hashicorp/consul 0.31.0 1.9.4 Official HashiCorp Consul Chart
+hashicorp/consul 0.30.0 1.9.3 Official HashiCorp Consul Chart
+hashicorp/consul 0.29.0 1.9.2 Official HashiCorp Consul Chart
+hashicorp/consul 0.28.0 1.9.1 Official HashiCorp Consul Chart
+hashicorp/consul 0.27.0 1.9.0 Official HashiCorp Consul Chart
+hashicorp/consul 0.26.0 1.8.5 Official HashiCorp Consul Chart
+hashicorp/consul 0.25.0 1.8.4 Official HashiCorp Consul Chart
+hashicorp/consul 0.24.1 1.8.2 Official HashiCorp Consul Chart
+hashicorp/consul 0.24.0 1.8.1 Official HashiCorp Consul Chart
+hashicorp/consul 0.23.1 1.8.0 Official HashiCorp Consul Chart
+hashicorp/consul 0.23.0 1.8.0 Official HashiCorp Consul Chart
+hashicorp/consul 0.22.0 1.8.0 Official HashiCorp Consul Chart
+hashicorp/consul 0.21.0 1.7.3 Official HashiCorp Consul Chart
+hashicorp/consul 0.20.1 1.7.2 Official HashiCorp Consul Chart
+
Kubernetes상에서 Consul Datacenter의 Gossip 프로토콜에서 사용할 키를 생성합니다. 미리 생성하여 값을 넣어도 되고, 생성시 값이 생성되도록 하여도 관계 없습니다.
$ kubectl create secret generic consul-gossip-encryption-key --from-literal=key=$(consul keygen)
+
+--- or ---
+
+$ consul keygen
+h65lqS3w4x42KP+n4Hn9RtK84Rx7zP3WSahZSyD5i1o=
+$ kubectl create secret generic consul-gossip-encryption-key --from-literal=key=h65lqS3w4x42KP+n4Hn9RtK84Rx7zP3WSahZSyD5i1o=
+
yaml
작성# consul.yaml
+global:
+ name: consul
+ # 기본이미지(OSS 최신 버전)가 아닌 다른 버전의 컨테이너 이미지 또는 별도의 레지스트리를 사용하는 경우 명시합니다.
+ image: 'hashicorp/consul-enterprise:1.8.5-ent'
+ datacenter: 'tsis-k8s'
+ tls:
+ # Federation 구성을 위해서는 TLS가 반드시 활성화되어야 합니다.
+ enabled: true
+ verify: false
+ httpsOnly: false
+
+ federation:
+ enabled: true
+ # Kubernetes가 Primary Datacenter이기 때문에 이 환경에서 Federation을 위한 시크릿을 생성하도록 합니다.
+ # https://www.consul.io/docs/k8s/installation/multi-cluster/kubernetes#primary-datacenter
+ createFederationSecret: true
+ gossipEncryption:
+ # gossip프로토콜은 암호화되어야 하며, 해당 키는 미리 Kubernetes에 Secret으로 구성합니다.
+ secretName: consul-gossip-encryption-key
+ secretKey: key
+ enableConsulNamespaces: true
+server:
+ enterpriseLicense:
+ secretName: consul-enterprise-license-key
+ secretKey: key
+connectInject:
+ enabled: true
+ centralConfig:
+ enabled: true
+ui:
+ service:
+ # UI에 접속을 위한 타입을 정의합니다.
+ # 보안상의 이유로 LoadBalancer기본적으로 서비스를 통해 노출되지 않으므로 kubectl port-forward를 사용하거나
+ # NodePort로 UI에 접속하는 데 사용해야 합니다.
+ type: NodePort
+dns:
+ enabled: true
+meshGateway:
+ # 메시 게이트웨이는 데이터 센터 간의 게이트웨이입니다.
+ # 데이터 센터 간의 통신이 메시 게이트웨이를 통과하므로 Kubernetes에서 페더레이션을 위해 활성화되어야합니다.
+ enabled: true
+ service:
+ type: NodePort
+ nodePort: 31001
+
+# Ingress Gateway는 Kubernets로 요청되는 주요 관문을 Consul에서 설정하고 Service Mesh기능을 활성화 합니다.
+# 이번 시나리오에서는 필수 설정이 아닙니다.
+ingressGateways:
+ enabled: true
+ gateways:
+ - name: ingress-gateway
+ service:
+ type: NodePort
+ ports:
+ - port: 31000
+ nodePort: 31000
+
Helm3을 사용하여 사용자 구성으로 Consul을 설치하려면 다음을 실행합니다. (사용자 구성 파일 : consul.yaml
)
$ helm install consul hashicorp/consul -f consul.yaml --debug
+
설치가 완료되고 얼마안있어 Pod를 확인해보면 다음과 같이 확인 가능합니다.
$ kubectl get pods
+consul-consul-mesh-gateway-754fbc5575-d8dgt 2/2 Running 0 2m
+consul-consul-mesh-gateway-754fbc5575-wkvjh 2/2 Running 0 2m
+consul-consul-mh5h6 1/1 Running 0 2m
+consul-consul-mx4mn 1/1 Running 0 2m
+consul-consul-rlb5x 1/1 Running 0 2m
+consul-consul-server-0 1/1 Running 0 2m
+consul-consul-server-1 1/1 Running 0 2m
+consul-consul-server-2 1/1 Running 0 2m
+consul-consul-tbngg 1/1 Running 1 2m
+consul-consul-tz9ct 1/1 Running 0 2m
+
port-forward
Consul UI에 접혹하기 위해 port-forward
를 사용하는 경우 다음과 같이 설정하여 접근가능합니다.
# HTTP
+$ kubectl port-forward service/consul-server 8500:8500
+
+# HTTPS (TLS)
+$ kubectl port-forward service/consul-server 8501:8501
+
ACL
ACL이 활성화된 경우 ACL토큰이 필요합니다. 전체 권한이 있는 bootstrap토큰은 다음과 같이 확인할 수 있습니다. (값의 마지막 %
는 제외)
$ kubectl get secrets/consul-bootstrap-acl-token --template={{.data.token}} | base64 -D
+e7924dd1-dc3f-f644-da54-81a73ba0a178%
+
Kubertnetes상에서 Mesh Gateway를 사용하기 위한 설정을 확인할 수 있도록 테스트를 위한 Pod를 생성합니다.
# k8s-consul-app.yaml
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: counting
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: counting
+ annotations:
+ 'consul.hashicorp.com/service-tags': servicemesh, consul, counting, v1
+ 'consul.hashicorp.com/connect-inject': 'true'
+spec:
+ containers:
+ - name: counting
+ image: hashicorp/counting-service:0.0.2
+ ports:
+ - containerPort: 9001
+ name: http
+ serviceAccountName: counting
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: dashboard
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: dashboard
+ labels:
+ app: 'dashboard'
+ annotations:
+ 'consul.hashicorp.com/service-tags': servicemesh, consul, dashiboard, v1
+ 'consul.hashicorp.com/connect-inject': 'true'
+ 'consul.hashicorp.com/connect-service-upstreams': 'counting:9001'
+spec:
+ containers:
+ - name: dashboard
+ image: hashicorp/dashboard-service:0.0.4
+ ports:
+ - containerPort: 9002
+ name: http
+ env:
+ - name: COUNTING_SERVICE_URL
+ value: 'http://localhost:9001'
+ serviceAccountName: dashboard
+---
+apiVersion: 'v1'
+kind: 'Service'
+metadata:
+ name: 'dashboard-service-nodeport'
+ namespace: 'default'
+ labels:
+ app: 'dashboard'
+spec:
+ ports:
+ - protocol: 'TCP'
+ port: 80
+ targetPort: 9002
+ selector:
+ app: 'dashboard'
+ type: 'NodePort'
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: dns
+spec:
+ containers:
+ - name: dns
+ image: anubhavmishra/tiny-tools
+ command:
+ - sleep
+ - infinity
+
'consul.hashicorp.com/connect-inject': 'true'
해당 annotations가 선언되면 consul api를 통해 해당 Pod가 배포될 때 Sidecar가 함께 생성됩니다.'consul.hashicorp.com/connect-service-upstreams': 'counting:9001'
설정은 side가에 9001포트로 요청이 오면 counting
으로 정의된 서비스로 해당 요청을 전달합니다.dashboard
앱은 frontend앱으로 UI를 제공하며, counting
앱은 backend앱으로 호출시 내부적으로 counting을 추가합니다.upstream
설정으로 9001에 대한 목적지를 알고 있는 sidecar container proxy가 해당 요청을 전달합니다.각 환경(Linux/Windows/Mac/FreeBSC/Solaris)에 맞는 바이너리를 받고 압축을 풉니다. consul
혹은 Windows의 경우 consul.exe
를 시스템의 적절한 위치에 이동시키고 PATH에 추가시키면 어느곳에서든 접근할 수 있습니다.
쉘 설정 파일을 편집하여 PATH에 영구적으로 추가할 수 있습니다. 일반적으로 .
+ 쉘이름
+ rc
로 구성되며 bash쉘을 사용하는 경우 ~/.bashrc
가 해당 파일입니다. 해당 파일에서 export PATH=
으로 시작하는 :(콜론)
으로 구분된 위치에 consul 바이너리 파일 위치를 넣어주거나 없는 경우 기존 PATH에 추가로 기입할 수 있습니다. /tools/consul_dir
디렉토리인경우 다음의 예와 같습니다.
...
+export PATH=$PATH:/tools/consul_dir
+
root 권한이 있다면 시스템의 기본 PATH로 지정되어있는 /usr/local/bin
디렉토리에 consul을 복사하는 것도 하나의 방법이 될 수 있습니다.
Windows
시스템 설정에서 GUI를 통해 PATH를 추가합니다. 마우스 클릭으로 진행하는 경우 Windows 설정 > 시스템 > 정보 > 시스템 정보 > 고급 시스템 설정 > 고급 탭 > 환경 변수
의 단계로 진행합니다. 작업표시줄의 검색창에서 고급 시스템 설정
을 검색하여 고급 탭 > 환경변수
로 이동할 수도 있습니다. 환경 변수 GUI에서 USER
또는 시스템 변수
의 Path에 Consul 디렉토리 경로를 추가합니다.
Kubernetes에 구성된 Consul Datacenter가 Primary이기 때문에 해당 환경에서 TLS 인증서를 가져옵니다. 앞서 구성된 Kubernetes 환경에서 CA(Certificate authority cert)와 서명 키(Certificate Authority signing key)를 가져옵니다.
$ kubectl get secrets/consul-ca-cert \\
+ --template='{{index .data "tls.crt" }}' | base64 -d > consul-agent-ca.pem
+$ kubectl get secrets/consul-ca-key \\
+ --template='{{index .data "tls.key" }}' | base64 -d > consul-agent-ca-key.pem
+
두 파일이 생성된 위치에서 consul tls
명령을 사용하여 서버에서 사용할 인증서를 생성합니다.
$ consul tls cert create -server -dc=vm-dc
+==> Using consul-agent-ca.pem and consul-agent-ca-key.pem
+==> Saved vm-dc-server-consul-0.pem
+==> Saved vm-dc-server-consul-0-key.pem
+
동일한 위치에서 Client를 위한 인증서를 생성합니다.
$ consul tls cert create -client -dc=vm-dc
+==> Using consul-agent-ca.pem and consul-agent-ca-key.pem
+==> Saved vm-dc-client-consul-0.pem
+==> Saved vm-dc-client-consul-0-key.pem
+
CA 파일과 새로 생성한 파일을 Server와 Client 각 환경에 복사합니다. (e.g. /home/consul/consul-cert/vm-dc-server-consul-0.pem)
앞서 생성한 파일 이름을 기준으로 복사 대상은 다음과 같습니다.
CLI를 활용하여 Consul을 구동할 때 구성 옵션을 사용하는 것도 가능하나 여기서는 구성 파일을 작성하여 Consul Server나 Consul Client가 기동할 수 있도록 합니다. Server와 Client에 대한 설정에 약간의 차이가 있을 뿐 대부분이 동일합니다.
server = true
+ui = true
+bootstrap_expect = 3
+node_name = "consul_server_01"
+datacenter = "vm-dc"
+client_addr = "0.0.0.0"
+bind_addr = "192.168.100.51"
+encrypt = "h65lqS3w4x42KP+n4Hn9RtK84Rx7zP3WSahZSyD5i1o="
+data_dir = "/var/lib/consul"
+retry_join = ["192.168.100.51","192.168.100.52","192.168.100.83"]
+ports {
+ https = 8501
+ http = 8500
+ grpc = 8502
+}
+enable_central_service_config = true
+connect {
+ enabled = true
+ enable_mesh_gateway_wan_federation = true
+}
+primary_datacenter = "k8s-dc"
+primary_gateways = ["172.16.1.111:31001","172.16.1.116:31001"]
+cert_file = "/root/consul-cert/vm-dc-server-consul-0.pem"
+key_file = "/root/consul-cert/vm-dc-server-consul-0-key.pem"
+ca_file = "/root/consul-cert/consul-agent-ca.pem"
+
server : server로 구성되는 Consul의 경우에 true
로 설정합니다.
node_name, bind_addr는 각 Server에 맞게 구성합니다. 여기서는 3개의 서버로 구성하였습니다.
encrypt : consul keygen
을 생성한 값입니다. Server와 Client모두 동일한 값을 설정합니다.
data_dir : Consul의 데이터를 저장할 경로이며 미리 생성해야 합니다.
retry_join : Consul 서버의 IP를 기입합니다.
ports: Mesh Gateway구성을 위해 https를 활성화 합니다.
enable_central_service_config : federation 구성을 위해 true
로 설정합니다.
connect : Service Mesh 구성 활성화를 위해 구성합니다. enable_mesh_gateway_wan_federation
는 Federation에서 Mesh Gateway를 활성화 시켜줍니다.
primary_datacenter : kubernetes 환경의 Datacenter이름을 기입합니다.
primary_gateways : Kubernetes 환경의 Mesh Gateway 의 IP와 Port를 기입합니다. 여기 예제에서는 Nodeport로 구성된 Consul Mesh Gateway의 값이 확인됩니다.
$ kubectl exec statefulset/consul-server -- sh -c 'curl -sk https://localhost:8501/v1/catalog/service/mesh-gateway | jq ".[].ServiceTaggedAddresses.wan"'
+{
+ "Address": "172.16.1.111",
+ "Port": 31001
+}
+{
+ "Address": "172.16.1.116",
+ "Port": 31001
+}
+
cert_file / key_file / ca_file : 앞서 생성한 Server 인증서들의 경로와 파일명을 기입합니다.
node_name = "consul_client_01"
+datacenter = "vm-dc"
+client_addr = "0.0.0.0"
+bind_addr = "192.168.100.54"
+encrypt = "h65lqS3w4x42KP+n4Hn9RtK84Rx7zP3WSahZSyD5i1o="
+data_dir = "/var/lib/consul"
+retry_join = ["192.168.100.51","192.168.100.52","192.168.100.53"]
+cert_file = "/root/consul-cert/vm-dc-client-consul-0.pem"
+key_file = "/root/consul-cert/vm-dc-client-consul-0-key.pem"
+ca_file = "/root/consul-cert/consul-agent-ca.pem"
+
Linux 환경이나 Windows환경에서 서비스로 구성하면 시스템 부팅 시 자동으로 시작할 수 있기 때문에 선호되는 설치 방식 중 하나입니다. 이미 설치된 상태라면 앞서 구성을 변경하고 consul reload
를 사용하여 구성을 다시 읽어오거나 리스타트 합니다.
/etc/systemd/system/consul.service
에 다음의 서비스 파일을 작성합니다. 필요에 따라 User와 Group을 추가하여 구성하는 것도 가능합니다. 여기서는 consul User를 구성하여 사용하였습니다.
[Unit]
+Description=Consul Service Discovery Agent
+Documentation=https://www.consul.io/
+After=network-online.target
+Wants=network-online.target
+
+[Service]
+Type=simple
+User=consul
+Group=consul
+ExecStart=/usr/local/bin/consul agent -config-dir=/etc/consul.d
+
+ExecReload=/bin/kill -HUP $MAINPID
+KillSignal=SIGINT
+TimeoutStopSec=5
+Restart=on-failure
+SyslogIdentifier=consul
+
+[Install]
+WantedBy=multi-user.target
+
등록된 서비스를 활성화 하고 시작하여 상대를 확인합니다.
$ systemctl enable consul
+$ systemctl start consul
+$ systemctl status consul
+● consul.service - Consul Service Discovery Agent
+ Loaded: loaded (/etc/systemd/system/consul.service; enabled; vendor preset: disabled)
+ Active: active (running) since 토 2020-11-14 19:05:39 UTC; 17h ago
+ Docs: https://www.consul.io/
+ Main PID: 1020 (consul)
+ CGroup: /system.slice/consul.service
+ └─1020 /usr/local/bin/consul agent -config-dir=/etc/consul.d
+
+$ journalctl -u consul -f
+11월 15 13:06:12 cl01-consul-vault-0 consul[1020]: 2020-11-15T13:06:12.265Z [INFO] agent.server: Handled event for server in area: event=member-join server=consul-consul-server-1.tsis-k8s area=wan
+11월 15 13:06:18 cl01-consul-vault-0 consul[1020]: 2020-11-15T13:06:18.119Z [INFO] agent.server.memberlist.wan: memberlist: Suspect consul-consul-server-0.tsis-k8s has failed, no acks received
+
Powershell을 활용하여 서비스를 구성합니다.
> sc.exe create "Consul" binPath= "consul agent -config-dir=C:\\ProgramData\\consul\\config" start= auto
+[SC] CreateService SUCCESS
+
+> sc.exe start "Consul"
+
+SERVICE_NAME: Consul
+ TYPE : 10 WIN32_OWN_PROCESS
+ STATE : 4 RUNNING (STOPPABLE, NOT_PAUSABLE, ACCEPTS_SHUTDOWN)
+ WIN32_EXIT_CODE : 0 (0x0)
+ SERVICE_EXIT_CODE : 0 (0x0)
+ CHECKPOINT : 0x0
+ WAIT_HINT : 0x0
+ PID : 8008
+ FLAGS :
+
Secondary Datacenter인 BM/VM 환경에서 primary_datacenter
를 지정하였기 때문에 기동 후 Kubernetes의 Consul과 Join되어 Federation이 구성됩니다.
Mesh Gateway를 구성하여 Service Mesh 환경이 멀티/하이브리드 Datacenter 환경을 지원하도록 합니다. Mesh Gateway는 Consul의 내장 Proxy로는 동작하지 못하므로 Envoy 를 설치하여 이를 활용합니다.
Consul의 각 버전별 지원하는 Envoy 버전은 다음 표와 같습니다.
Consul Version | Compatible Envoy Versions |
---|---|
1.10.x | 1.18.3, 1.17.3, 1.16.4, 1.15.5 |
1.9.x | 1.16.0, 1.15.2, 1.14.5‡, 1.13.6‡ |
1.8.x | 1.14.5, 1.13.6, 1.12.7, 1.11.2 |
1.7.x | 1.13.6, 1.12.7, 1.11.2, 1.10.0* |
1.6.x, 1.5.3, 1.5.2 | 1.11.1, 1.10.0, 1.9.1, 1.8.0† |
1.5.1, 1.5.0 | 1.9.1, 1.8.0† |
1.4.x, 1.3.x | 1.9.1, 1.8.0†, 1.7.0† |
다음 명령을 실행하여 Envoy를 가져와 설치하는 func-e
유틸리티를 다운로드하고 설치합니다.
curl -L https://func-e.io/install.sh | bash -s -- -b /usr/local/bin
+
다음과 같이 대상 환경을 지정할 수 있습니다.
export FUNC_E_PLATFORM=darwin/amd64
+
go
out of the boxaix/ppc64
darwin/386
darwin/amd64
dragonfly/amd64
freebsd/386
freebsd/amd64
freebsd/arm
freebsd/arm64
illumos/amd64
js/wasm
linux/386
linux/amd64
linux/arm
linux/arm64
linux/ppc64
linux/ppc64le
linux/mips
linux/mipsle
linux/mips64
linux/mips64le
linux/riscv64
linux/s390x
netbsd/386
netbsd/amd64
netbsd/arm
netbsd/arm64
openbsd/386
openbsd/amd64
openbsd/arm
openbsd/arm64
plan9/386
plan9/amd64
plan9/arm
solaris/amd64
windows/386
windows/amd64
windows/arm
go
out of the boxdarwin/386
freebsd/386
freebsd/arm
linux/386
linux/arm
linux/mips
linux/mipsle
netbsd/386
netbsd/arm
openbsd/386
openbsd/arm
plan9/386
plan9/arm
windows/386
windows/arm
go
out of the boxaix/ppc64
darwin/amd64
dragonfly/amd64
freebsd/amd64
freebsd/arm64
illumos/amd64
js/wasm
linux/amd64
linux/arm64
linux/ppc64
linux/ppc64le
linux/mips64
linux/mips64le
linux/riscv64
linux/s390x
netbsd/amd64
netbsd/arm64
openbsd/amd64
openbsd/arm64
plan9/amd64
solaris/amd64
windows/amd64
특정 버전을 명시하여 다운로드 하려면 다음 명령을 실행합니다.
func-e use 1.18.3
+
Envoy 바이너리를 $PATH
의 위치에 복사합니다. 이를 통해 Consul은 바이너리 위치를 지정하지 않고 Envoy를 자동으로 시작할 수 있습니다.
sudo cp ~/.func-e/versions/1.18.3/bin/envoy /usr/local/bin/
+
다음 명령을 실행하여 Envoy가 $PATH
에 있는지 확인합니다.
envoy --version
+
Mesh Gateway는 TLS를 필요로하며 Consul과도 TLS로 통신 합니다. 따라서 Consul로의 기본 접속 방식과 포트를 SSL기준으로 설정하여 실행합니다. 또한 앞서 Consul Client를 위해 생성한 인증서를 활용합니다.
$ export CONSUL_HTTP_SSL=true
+$ export CONSUL_HTTP_ADDR=https://127.0.0.1:8501
+$ consul connect envoy -gateway=mesh -register -expose-servers \\
+ -service "mesh-gateway-secondary" \\
+ -ca-file=/root/consul-cert/consul-agent-ca.pem \\
+ -client-cert=/root/consul-cert/vm-dc-client-consul-0.pem \\
+ -client-key=/root/consul-cert/vm-dc-client-consul-0-key.pem \\
+ -address '{{ GetInterfaceIP "lo" }}:9100' \\
+ -wan-address '{{ GetInterfaceIP "eth0" }}:9100' -admin-bind=127.0.0.1:19001 &
+
&
를 붙였습니다. 원하지 않으시면 제거하여 포그라운드로 띄우셔도 됩니다.실행 후에는 Consul UI에서도 해당 Mesh Gateway를 확인할 수 있습니다.
앞서 2.2.6 테스트를 위한 Pod 생성의 counting 서비스를 활용합니다.
apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: counting
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: counting
+ annotations:
+ 'consul.hashicorp.com/service-tags': servicemesh, consul, counting, v1
+ 'consul.hashicorp.com/connect-inject': 'true'
+spec:
+ containers:
+ - name: counting
+ image: hashicorp/counting-service:0.0.2
+ ports:
+ - containerPort: 9001
+ name: http
+ serviceAccountName: counting
+
BM/VM 환경에서 Frontend 애플리케이션을 구성합니다. Envoy Proxy를 Sidecar로 구성하여 Service Mesh를 위한 구성을 하고, 다른 Consul 데이터센터에 있는 서비스를 찾을 수 있도록 합니다.
`,6),F={href:"https://github.com/hashicorp/demo-consul-101/tree/master/services/dashboard-service",target:"_blank",rel:"noopener noreferrer"},W=n("p",null,"애플리케이션 실행을 위해서는 golang이 설치되어야 합니다.",-1),j=t(`Dashboard 애플리케이션을 실행합니다.
$ PORT=9003 COUNTING_SERVICE_URL="http://localhost:5000" go run main.go &
+
9003으로 실행된 Dashboard 애플리케이션을 Consul에 서비스로 등록합니다. Consul의 기본 Configuration 디렉토리 위치에 해당 서비스 구성을 작성하고 읽어올 수 있습니다. (e.g. /etc/consul.d)
# /etc/consul.d/dashboard.hcl
+service {
+ name = "dashboard-vm"
+ port = 9003
+
+ connect {
+ sidecar_service {
+ proxy {
+ upstreams = [
+ {
+ destination_name = "counting"
+ datacenter = "k8s-dc"
+ local_bind_port = 5000
+ mesh_gateway {
+ mode = "local"
+ }
+ }
+ ]
+ }
+ }
+ }
+
+ check {
+ id = "dashboard-check"
+ http = "http://localhost:9003/health"
+ method = "GET"
+ interval = "1s"
+ timeout = "1s"
+ }
+}
+
Dashboard 애플리케이션에서 COUNTING_SERVICE_URL
의 대상을 5000번 포트로 지정하였기 때문에 upstream에서 바인딩되는 포트를 맞춰줍니다. 구성이 완료되면 consul reload
명령을 통해 구성 디렉토리의 파일을 반영하고 추가된 서비스를 확인합니다.
$ consul reload
+Configuration reload triggered
+
+$ consul catalog services
+consul
+dashboard-vm
+
다음으로 Dashboard 서비스를 위한 Sidecar를 실행하고 추가된 서비스를 확인합니다.
$ consul connect envoy -sidecar-for dashboard-vm \\
+ -ca-file=/root/consul-cert/consul-agent-ca.pem \\
+ -client-cert=/root/consul-cert/vm-dc-client-consul-0.pem \\
+ -client-key=/root/consul-cert/vm-dc-client-consul-0-key.pem &
+
+$ consul catalog services
+consul
+dashboard-vm
+dashboard-vm-sidecar-proxy
+
이제 구성된 9003번 포트를 통해 Frontend에서 외부 데이터센터의 Backend로 요청이 되는지 확인합니다.
Sidecar기능이 활성화 되면서 Consul의 Intention기능을 사용할 수 있습니다. Intention을 통해 동적으로 서비스에 대한 트래픽을 통제할 수 있습니다.
UI 또는 CLI를 통해 dashboard-vm
이 counting
에 접근할 수 없도록 정의합니다.
$ consul intention create -deny -replace dashboard-vm counting
+Created: dashboard-vm => counting (deny)
+
접근할 수 없게 설정되었기 때문에 Sidecar에 주입된 설정으로 Dashboard에서는 Counting서비스에 접근할 수 없다는 메시지를 출력합니다.
`,16);function J(Y,Q){const a=o("ExternalLinkIcon");return i(),c("div",null,[r,n("blockquote",null,[n("p",null,[s("Port 구성에 대한 문서는 다음을 참고합니다."),u,n("a",d,[s("https://www.consul.io/docs/install/ports"),e(a)])])]),k,n("p",null,[s("각 "),n("a",m,[s("포트 구성설정 가이드"),e(a)]),s("는 다음과 같습니다.")]),v,n("ul",null,[b,n("li",null,[s("Helm3 혹은 Helm2를 설치합니다. "),n("ul",null,[n("li",null,[s("Installing Helm : "),n("a",h,[s("https://helm.sh/docs/intro/install"),e(a)])])])])]),g,n("p",null,[s("Kubernetes에서 Consul을 실행하는 권장 방법은 "),n("a",y,[s("Helm 차트를 사용하는 것"),e(a)]),s(" 입니다. Consul을 실행하는 데 필요한 모든 구성 요소를 설치하고 구성합니다. Helm 2를 사용하는 경우 "),n("a",f,[s("Helm 2 설치 가이드"),e(a)]),s(" 에 따라 Tiller를 설치해야합니다.")]),_,n("p",null,[s("Helm 차트로 설치할 때 기본 설정을 엎어쓰는 파일을 생성하여 원하는 구성으로 설치되도록 준비합니다. 각 구성에 대한 설정은 "),n("a",C,[s("Helm Chart Configuration"),e(a)]),s(" 를 참고합니다.")]),x,n("ul",null,[n("li",null,[s("Consul 을 실행할 수 있도록 준비합니다. "),n("ul",null,[n("li",null,[s("Install Consul : "),n("a",w,[s("https://www.consul.io/docs/install"),e(a)])]),n("li",null,[s("BM이나 VM 환경에 Consul을 설치하는 방법은 "),n("a",S,[s("미리 컴파일 된 바이너리"),e(a)]),s("를 사용하여 구성하거나 "),n("a",P,[s("소스"),e(a)]),s("로부터 컴파일하여 사용하는 두가지 방법이 있습니다. 사전 컴파일 된 바이너리를 다운로드하여 사용하는 방법이 가장 쉽습니다. 이번 환경에서는 컴파일된 바이너리를 기준으로 설명합니다.")])])])]),q,n("blockquote",null,[n("p",null,[s("Federation Between VMs and Kubernetes : "),n("a",T,[s("https://www.consul.io/docs/k8s/installation/multi-cluster/vms-and-kubernetes"),e(a)])])]),M,n("div",E,[G,n("p",null,[s("‡ Consul 1.9.x는 1.15.0+의 Envoy를 권장합니다."),A,s(" † 1.9.1 버전 이하의 Envoy는 "),n("a",I,[s("CVE-2019-9900"),e(a)]),s(", "),n("a",H,[s("CVE-2019-9901"),e(a)]),s(" 취약점이 보고되었습니다."),O,s(" * Consul 1.7.x에서 Envoy 1.10.0을 사용하는 경우 "),R,s(" 커맨드 사용시 "),D,s(" 옵션을 포함해야합니다.")])]),n("p",null,[n("a",N,[s("Envoy 웹사이트"),e(a)]),s(" 에서 직접 Envoy의 컨테이너 기반 빌드를 얻거나 "),n("a",K,[s("func-e.io"),e(a)]),s(" 와 같은 3rd party 프로젝트에서 Envoy 바이너리 빌드 패키지를 얻을 수 있습니다.")]),V,n("blockquote",null,[n("p",null,[s("Frontend 애플리케이션을 BM/VM 환경에 구성하고 Backend를 Kubernetes에 구성하는 예제입니다."),L,n("a",U,[s("https://github.com/hashicorp/demo-consul-101"),e(a)])])]),n("details",B,[$,n("blockquote",null,[n("p",null,[n("a",F,[s("https://github.com/hashicorp/demo-consul-101/tree/master/services/dashboard-service"),e(a)])]),W]),j,n("ul",null,[z,n("li",null,[s('COUNTING_SERVICE_URL : Backend 애플리케이션인 Counting Service에 대한 정보입니다. 없는 경우 기본 값은 "'),n("a",Z,[s("http://localhost:9001"),e(a)]),s('" 입니다.')])]),X])])}const an=l(p,[["render",J],["__file","Consul Enterprise Feature.html.vue"]]),en=JSON.parse('{"path":"/04-HashiCorp/04-Consul/03-UseCase/Consul%20Enterprise%20Feature.html","title":"Consul Mesh Gateway - K8S x BMs/VMs","lang":"ko-KR","frontmatter":{"description":"Mesh Gateway k8s and VM","tag":["Consul","Hybrid","Kubetenetes","k8s","VM"],"head":[["meta",{"property":"og:url","content":"https://docmoa.github.io/04-HashiCorp/04-Consul/03-UseCase/Consul%20Enterprise%20Feature.html"}],["meta",{"property":"og:site_name","content":"docmoa"}],["meta",{"property":"og:title","content":"Consul Mesh Gateway - K8S x BMs/VMs"}],["meta",{"property":"og:description","content":"Mesh Gateway k8s and VM"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://raw.githubusercontent.com/Great-Stone/images/master/uPic/HashiCorp%20General%20Presentation%20Template%20%28KR%29%20-%20Apr%202020%20-%20Google%20Slides%202020-11-12%2015-58-54.png"}],["meta",{"property":"og:locale","content":"ko-KR"}],["meta",{"property":"og:updated_time","content":"2023-09-18T13:12:54.000Z"}],["meta",{"name":"twitter:card","content":"summary_large_image"}],["meta",{"name":"twitter:image:alt","content":"Consul Mesh Gateway - K8S x BMs/VMs"}],["meta",{"property":"article:tag","content":"Consul"}],["meta",{"property":"article:tag","content":"Hybrid"}],["meta",{"property":"article:tag","content":"Kubetenetes"}],["meta",{"property":"article:tag","content":"k8s"}],["meta",{"property":"article:tag","content":"VM"}],["meta",{"property":"article:modified_time","content":"2023-09-18T13:12:54.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Consul Mesh Gateway - K8S x BMs/VMs\\",\\"image\\":[\\"https://raw.githubusercontent.com/Great-Stone/images/master/uPic/HashiCorp%20General%20Presentation%20Template%20%28KR%29%20-%20Apr%202020%20-%20Google%20Slides%202020-11-12%2015-58-54.png\\",\\"https://raw.githubusercontent.com/Great-Stone/images/master/uPic/DESKTOP-LenovoMini%202020-11-15%2020-36-36.png\\",\\"https://raw.githubusercontent.com/Great-Stone/images/master/uPic/consul-datacenter-dropdown.png\\",\\"https://learn.hashicorp.com/img/consul/connect-getting-started/screenshot1.png\\",\\"https://raw.githubusercontent.com/Great-Stone/images/master/uPic/Edit%20Intention%20-%20Consul%202020-11-16%2000-27-50.png\\",\\"https://learn.hashicorp.com/img/consul/connect-getting-started/screenshot2.png\\"],\\"dateModified\\":\\"2023-09-18T13:12:54.000Z\\",\\"author\\":[]}"]]},"headers":[{"level":2,"title":"1. 개요","slug":"_1-개요","link":"#_1-개요","children":[{"level":3,"title":"1.1 아키텍처","slug":"_1-1-아키텍처","link":"#_1-1-아키텍처","children":[]},{"level":3,"title":"1.2 Port 구성 참고","slug":"_1-2-port-구성-참고","link":"#_1-2-port-구성-참고","children":[]}]},{"level":2,"title":"2. Kubernetes상에 Consul설치","slug":"_2-kubernetes상에-consul설치","link":"#_2-kubernetes상에-consul설치","children":[{"level":3,"title":"2.1 사전 준비 사항","slug":"_2-1-사전-준비-사항","link":"#_2-1-사전-준비-사항","children":[]},{"level":3,"title":"2.2 설치","slug":"_2-2-설치","link":"#_2-2-설치","children":[]}]},{"level":2,"title":"3. VM/BM상에 Consul설치","slug":"_3-vm-bm상에-consul설치","link":"#_3-vm-bm상에-consul설치","children":[{"level":3,"title":"3.1 사전 준비 사항","slug":"_3-1-사전-준비-사항","link":"#_3-1-사전-준비-사항","children":[]},{"level":3,"title":"3.2 Consul 바이너리 다운로드와 PATH 설정","slug":"_3-2-consul-바이너리-다운로드와-path-설정","link":"#_3-2-consul-바이너리-다운로드와-path-설정","children":[]},{"level":3,"title":"3.3 Primary(k8s) 환경에서 인증서 가져오기","slug":"_3-3-primary-k8s-환경에서-인증서-가져오기","link":"#_3-3-primary-k8s-환경에서-인증서-가져오기","children":[]},{"level":3,"title":"3.4 Consul 구성 파일 작성","slug":"_3-4-consul-구성-파일-작성","link":"#_3-4-consul-구성-파일-작성","children":[]}]},{"level":2,"title":"4. BM/VM 환경의 Mesh Gateway 구성","slug":"_4-bm-vm-환경의-mesh-gateway-구성","link":"#_4-bm-vm-환경의-mesh-gateway-구성","children":[{"level":3,"title":"4.1 Envoy 설치","slug":"_4-1-envoy-설치","link":"#_4-1-envoy-설치","children":[]},{"level":3,"title":"4.2 Mesh Gateway 실행","slug":"_4-2-mesh-gateway-실행","link":"#_4-2-mesh-gateway-실행","children":[]}]},{"level":2,"title":"5 TEST (Option)","slug":"_5-test-option","link":"#_5-test-option","children":[]}],"git":{"createdTime":1628557352000,"updatedTime":1695042774000,"contributors":[{"name":"Great-Stone","email":"hahohh@gmail.com","commits":2}]},"readingTime":{"minutes":8.05,"words":2415},"filePathRelative":"04-HashiCorp/04-Consul/03-UseCase/Consul Enterprise Feature.md","localizedDate":"2021년 8월 10일","excerpt":"\\n\\n\\n이 문서에서는 Consul을 사용하여 상이한 두 Consul로 구성된 클러스터(마스터가 별개)의 서비스를 연계하는 방법을 설명합니다.
\\n
네트워크 영역이 분리되어있는 두 환경의 애플리케이션 서비스들을 Service Mesh로 구성하는 방법을 알아 봅니다. 이번 구성 예에서는 Kubernetes와 Baremetal(BM)이나 VirtualMachine(VM)에 Consul Cluster(Datacenter)를 구성하고 각 환경의 애플리케이션 서비스를 Mesh Gateway로 연계합니다.
"}');export{an as comp,en as data}; diff --git a/assets/Consul Health Check.html-DGZ9Ux0W.js b/assets/Consul Health Check.html-DGZ9Ux0W.js new file mode 100644 index 0000000000..cfbf09623c --- /dev/null +++ b/assets/Consul Health Check.html-DGZ9Ux0W.js @@ -0,0 +1,18 @@ +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{r as c,o as a,c as l,b as e,d as t,a as n,e as r}from"./app-Bzk8Nrll.js";const i={},h=e("h1",{id:"consul-health-check-on-vms",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#consul-health-check-on-vms"},[e("span",null,"Consul Health Check on VMs")])],-1),u={href:"https://www.consul.io/docs/discovery/services",target:"_blank",rel:"noopener noreferrer"},d=e("br",null,null,-1),p={href:"https://learn.hashicorp.com/tutorials/consul/service-registration-health-checks?in=consul/developer-discovery#tuning-scripts-to-be-compatible-with-consul",target:"_blank",rel:"noopener noreferrer"},m=r(`Consul config 디렉토리 하위에 monitor.hcl파일을 만듭니다.
services {
+ id = "web-service"
+ namd = "web-service"
+ address = "10.10.10.201"
+ port = 8080
+ checks = [
+ {
+ script = "/opt/consul/script/ps-check.sh"
+ interval = "180s"
+ }
+ ]
+}
+
Consul에서 스크립트 기반의 설정시 Config파일 내에 하기와 같은 옵션이 추가되어야 합니다.
(기존설정)
+.....
+enable_script_checks = "true" 또는
+enable_local_script_checks = "true"
+
+
\\n","copyright":{"author":"euimokna"}}');export{f as comp,w as data}; diff --git a/assets/Consul Install.html-CqnmzIZZ.js b/assets/Consul Install.html-CqnmzIZZ.js new file mode 100644 index 0000000000..974a4852db --- /dev/null +++ b/assets/Consul Install.html-CqnmzIZZ.js @@ -0,0 +1,5 @@ +import{_ as s}from"./plugin-vue_export-helper-DlAUqK2U.js";import{o as n,c as a,e}from"./app-Bzk8Nrll.js";const t={},o=e(`https://www.consul.io/docs/discovery/services
\\n
\\nhttps://learn.hashicorp.com/tutorials/consul/service-registration-health-checks?in=consul/developer-discovery#tuning-scripts-to-be-compatible-with-consul
AmazonLinux 환경에서 하기와 같은 명령어로 consul 설치 후 systemd 를 통한 Consul 시작시 오류 발생
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo
+sudo yum -y install consul
+
[ec2-user@ip-10-0-10-35 ~]$ sudo systemctl start consul
+Job for consul.service failed because a configured resource limit was exceeded. See "systemctl status consul.service" and "journalctl -xe" for details.
+
yum 명령어로 consul설치시 /etc/consul.d/ 경로에 기본적으로 consul.env
파일이 자동으로 생성되는데 해당 파일이 생성되지 않아 수동으로 생성함.
AmazonLinux 환경에서 하기와 같은 명령어로 consul 설치 후 systemd 를 통한 Consul 시작시 오류 발생
\\nsudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo\\nsudo yum -y install consul\\n
annotations:
+ 'consul.hashicorp.com/connect-inject': 'true'
+
Consul Sidecar가 Pod 배포시 함께 구성되야 하는 것이 정상이나, Sidecar의 생성 실패나 이미지 가져오기 실패라는 언급도 없이 Sidecar의 injection이 동작하지 않는 경우가 있다.
쿠버네티스 상의 Consul을 구성하게 되면 injector가 Sidecar를 함께 배포하는 작업을 수행하므로 먼저 해당 컴포넌트의 로그를 확인한다.
kubectl logs -n consul -l component=connect-injector -f
+
annotation
의 동작은 쿠버네티스 컨트롤 플래인, 즉, 쿠버네티스의 API를 통해 요청되므로 해당 API를 통해 Consul에 접근이 가능한지 확인이 필요하다.
consul-inject에서 kubernetest api 접속이 불가하다면 500
에러가 발생한다.
$ kubectl proxy
+Starting to serve on 127.0.0.1:8001
+
$ curl -vv localhost:8001/api/v1/namespaces/consul/services/https:consul-connect-injector-svc:443/proxy/health/ready
+* Trying 127.0.0.1...
+* TCP_NODELAY set
+* Connected to localhost (127.0.0.1) port 8001 (#0)
+> GET /api/v1/namespaces/consul/services/https:consul-connect-injector-svc:443/proxy/health/ready HTTP/1.1
+> Host: localhost:8001
+> User-Agent: curl/7.61.1
+> Accept: */*
+>
+< HTTP/1.1 204 No Content
+< Audit-Id: 52947d1d-0c90-47eb-8dc2-6c2efa0193fa
+< Cache-Control: no-cache, private
+< Date: Fri, 06 Aug 2021 10:15:21 GMT
+<
+* Connection #0 to host localhost left intact
+
* Trying 127.0.0.1...
+* TCP_NODELAY set
+* Connected to localhost (127.0.0.1) port 8001 (#0)
+> GET /api/v1/namespaces/consul/services/https:consul-connect
+-injector-svc:443/proxy/health/ready HTTP/1.1
+> Host: localhost:8001
+> User-Agent: curl/7.61.1
+> Accept: */*
+>
+< HTTP/1.1 500 Internal Server Error
+< Audit-Id: acb30d91-d8db-463e-a91e-1e2a5382329e
+< Cache-Control: no-cache, private
+< Content-Length: 178
+< Content-Type: application/json
+< Date: Fri, 06 Aug 2021 11:04:38 GMT
+<
+{
+ "kind": "Status",
+ "apiVersion": "v1",
+ "metadata": {
+
+ },
+ "status": "Failure",
+ "message": "error trying to reach service: Address is not a
+ llowed",
+ "code": 500
+}
+* Connection #0 to host localhost left intact
+
\\n\\nConsul Version : 1.9.x
\\n
\\nHelm Chart : 0.30.0
Consul을 쿠버네티스 상에 구성하게 되면 annotation
구성만으로도 쉽게 Sidecar를 애플리케이션과 함께 배포 가능하다.
참고 : Controlling Injection Via Annotation
"}');export{S as comp,_ as data}; diff --git a/assets/CredentialConfig.html-B_-qDT_l.js b/assets/CredentialConfig.html-B_-qDT_l.js new file mode 100644 index 0000000000..0dc05872c6 --- /dev/null +++ b/assets/CredentialConfig.html-B_-qDT_l.js @@ -0,0 +1,47 @@ +import{_ as l}from"./plugin-vue_export-helper-DlAUqK2U.js";import{r as o,o as i,c,b as n,d as e,a as s,e as t}from"./app-Bzk8Nrll.js";const r={},p=t('AccessKey 방식의 인증 정보가 없는 경우 아래와 같이 생성
Accesskey Management
클릭AccessKey Pair
항목에서 Create AccessKey Pair
를 클릭하여 AK(AccessKey)를 신규로 생성aliyun cli의 configure
를 실행
--mode
에는 Credential Type을 지정--profile
에는 사용할 이름을 사용자가 지정, default
는 기본 값$ aliyun configure --mode AK --profile myprofile
+Configuring profile 'myprofile' in 'AK' authenticate mode...
+Access Key Id []: LTAI5**********************t88V
+Access Key Secret []: *******************************
+Default Region Id []: ap-southeast-1
+Default Output Format [json]: json (Only support json)
+Default Language [zh|en] en: en
+Saving profile[gslee] ...Done.
+
+Configure Done!!!
+
구성 완료 후 home 디렉토리의 .aliyun
디렉토리 내의 config.json
에서 구성된 정보를 확인 가능
{
+ "current": "myprofile",
+ "profiles": [
+ {
+ "name": "default",
+ },
+ {
+ "name": "gslee",
+ "mode": "AK",
+ "access_key_id": "LTAI5**********************t88V",
+ "region_id": "ap-southeast-1",
+ "output_format": "json",
+ "language": "en",
+ }
+ ],
+ "meta_path": ""
+}
+
Region 목록 확인
팁
profile이 default
인 경우 --profile or -p
생략 가능
$ aliyun -p myprofile ecs DescribeRegions
+{
+ "Regions": {
+ "Region": [
+ {
+ "LocalName": "华北1(青岛)",
+ "RegionEndpoint": "ecs.aliyuncs.com",
+ "RegionId": "cn-qingdao"
+ },
+ {
+ "LocalName": "华北2(北京)",
+ "RegionEndpoint": "ecs.aliyuncs.com",
+ "RegionId": "cn-beijing"
+ },
+ ]
+ },
+ "RequestId": "2304DF19-CABF-54DF-BDC6-F889C3A73E4F"
+}
+
팁
테스트 및 사용전 확인해야 할 사항은 Nomad의 enterprise, 즉 라이선스가 필요하며, nomad-autosclaer의 경우에도 enterprise여야만 합니다.
job "example" {
+ datacenters = ["dc1"]
+
+ group "cache-lb" {
+ count = 1
+
+ network {
+ port "lb" {}
+ }
+
+ task "nginx" {
+ driver = "docker"
+
+ config {
+ image = "nginx"
+ ports = ["lb"]
+ volumes = [
+ # It's safe to mount this path as a file because it won't re-render.
+ "local/nginx.conf:/etc/nginx/nginx.conf",
+ # This path hosts files that will re-render with Consul Template.
+ "local/nginx:/etc/nginx/conf.d"
+ ]
+ }
+
+ # This template overwrites the embedded nginx.conf file so it loads
+ # conf.d/*.conf files outside of the \`http\` block.
+ template {
+ data = <<EOF
+user nginx;
+worker_processes 1;
+error_log /var/log/nginx/error.log warn;
+pid /var/run/nginx.pid;
+events {
+ worker_connections 1024;
+}
+include /etc/nginx/conf.d/*.conf;
+EOF
+ destination = "local/nginx.conf"
+ }
+
+ # This template creates a TCP proxy to Redis.
+ template {
+ data = <<EOF
+stream {
+ server {
+ listen {{ env "NOMAD_PORT_lb" }};
+ proxy_pass backend;
+ }
+ upstream backend {
+ {{ range nomadService "redis" }}
+ server {{ .Address }}:{{ .Port }};
+ {{ else }}server 127.0.0.1:65535; # force a 502
+ {{ end }}
+ }
+}
+EOF
+ destination = "local/nginx/nginx.conf"
+ change_mode = "signal"
+ change_signal = "SIGHUP"
+ }
+
+ resources {
+ cpu = 50
+ memory = 10
+ }
+
+ scaling "cpu" {
+ policy {
+ cooldown = "1m"
+ evaluation_interval = "10s"
+
+ check "95pct" {
+ strategy "app-sizing-percentile" {
+ percentile = "95"
+ }
+ }
+ }
+ }
+
+ scaling "mem" {
+ policy {
+ cooldown = "1m"
+ evaluation_interval = "10s"
+
+ check "max" {
+ strategy "app-sizing-max" {}
+ }
+ }
+ }
+ }
+
+ service {
+ name = "redis-lb"
+ port = "lb"
+ address_mode = "host"
+ provider = "nomad"
+ }
+ }
+
+ group "cache" {
+ count = 3
+
+ network {
+ port "db" {
+ to = 6379
+ }
+ }
+
+ task "redis" {
+ driver = "docker"
+
+ config {
+ image = "redis:6.0"
+ ports = ["db"]
+ }
+
+ resources {
+ cpu = 500
+ memory = 256
+ }
+
+ scaling "cpu" {
+ policy {
+ cooldown = "1m"
+ evaluation_interval = "10s"
+
+ check "95pct" {
+ strategy "app-sizing-percentile" {
+ percentile = "95"
+ }
+ }
+ }
+ }
+
+ scaling "mem" {
+ policy {
+ cooldown = "1m"
+ evaluation_interval = "10s"
+
+ check "max" {
+ strategy "app-sizing-max" {}
+ }
+ }
+ }
+
+ service {
+ name = "redis"
+ port = "db"
+ address_mode = "host"
+ provider = "nomad"
+ }
+ }
+ }
+}
+
job "das-load-test" {
+ datacenters = ["dc1"]
+ type = "batch"
+
+ parameterized {
+ payload = "optional"
+ meta_optional = ["requests", "clients"]
+ }
+
+ group "redis-benchmark" {
+ task "redis-benchmark" {
+ driver = "docker"
+
+ config {
+ image = "redis:6.0"
+ command = "redis-benchmark"
+
+ args = [
+ "-h",
+ "\${HOST}",
+ "-p",
+ "\${PORT}",
+ "-n",
+ "\${REQUESTS}",
+ "-c",
+ "\${CLIENTS}",
+ ]
+ }
+
+ template {
+ destination = "secrets/env.txt"
+ env = true
+
+ data = <<EOF
+{{ with nomadService "redis-lb" }}{{ with index . 0 -}}
+HOST={{.Address}}
+PORT={{.Port}}
+{{- end }}{{ end }}
+REQUESTS={{ or (env "NOMAD_META_requests") "100000" }}
+CLIENTS={{ or (env "NOMAD_META_clients") "50" }}
+EOF
+ }
+
+ resources {
+ cpu = 100
+ memory = 128
+ }
+ }
+ }
+}
+
#systemd-resolved 설정파일 추가 및 변경
+mkdir -p /etc/systemd/resolved.conf.d
+(
+cat <<-EOF
+[Resolve]
+DNS=127.0.0.1
+DNSSEC=false
+Domains=~consul
+EOF
+) | sudo tee /etc/systemd/resolved.conf.d/consul.conf
+(
+cat <<-EOF
+nameserver 127.0.0.1
+options edns0 trust-ad
+EOF
+) | sudo tee /etc/resolv.conf
+#iptables에 consul dns port 추가
+iptables --table nat --append OUTPUT --destination localhost --protocol udp --match udp --dport 53 --jump REDIRECT --to-ports 8600
+iptables --table nat --append OUTPUT --destination localhost --protocol tcp --match tcp --dport 53 --jump REDIRECT --to-ports 8600
+#service 재시작
+systemctl restart systemd-resolved
+
#Global domain에 consul 확인
+$ resolvectl domain
+Global: ~consul
+Link 5 (docker0):
+Link 4 (eth2):
+Link 3 (eth1):
+Link 2 (eth0):
+#consul service확인, 해당 클러스터에는 consul server가 3대임
+$ resolvectl query consul.service.consul
+consul.service.consul: 172.30.1.100
+ 172.30.1.101
+ 172.30.1.102
+
+
#systemd-resolved 설정파일 추가 및 변경\\nmkdir -p /etc/systemd/resolved.conf.d\\n(\\ncat <<-EOF\\n[Resolve]\\nDNS=127.0.0.1\\nDNSSEC=false\\nDomains=~consul\\nEOF\\n) | sudo tee /etc/systemd/resolved.conf.d/consul.conf\\n(\\ncat <<-EOF\\nnameserver 127.0.0.1\\noptions edns0 trust-ad\\nEOF\\n) | sudo tee /etc/resolv.conf\\n#iptables에 consul dns port 추가\\niptables --table nat --append OUTPUT --destination localhost --protocol udp --match udp --dport 53 --jump REDIRECT --to-ports 8600\\niptables --table nat --append OUTPUT --destination localhost --protocol tcp --match tcp --dport 53 --jump REDIRECT --to-ports 8600\\n#service 재시작\\nsystemctl restart systemd-resolved\\n
variable "base_image" {
+ default = "ubuntu-1804-bionic-v20210415"
+}
+variable "project" {
+ default = "gs-test-282101"
+}
+variable "region" {
+ default = "asia-northeast2"
+}
+variable "zone" {
+ default = "asia-northeast2-a"
+}
+variable "image_name" {
+
+}
+variable "placeholder" {
+ default = "placekitten.com"
+ description = "Image-as-a-service URL. Some other fun ones to try are fillmurray.com, placecage.com, placebeard.it, loremflickr.com, baconmockup.com, placeimg.com, placebear.com, placeskull.com, stevensegallery.com, placedog.net"
+}
+
+source "googlecompute" "basic-example" {
+ project_id = var.project
+ source_image = var.base_image
+ ssh_username = "ubuntu"
+ zone = var.zone
+ disk_size = 10
+ disk_type = "pd-ssd"
+ image_name = var.image_name
+}
+
+build {
+ name = "packer"
+ source "sources.googlecompute.basic-example" {
+ name = "packer"
+ }
+
+ provisioner "file"{
+ source = "./files"
+ destination = "/tmp/"
+ }
+
+ provisioner "shell" {
+ inline = [
+ "sudo apt-get -y update",
+ "sleep 15",
+ "sudo apt-get -y update",
+ "sudo apt-get -y install apache2",
+ "sudo systemctl enable apache2",
+ "sudo systemctl start apache2",
+ "sudo chown -R ubuntu:ubuntu /var/www/html",
+ "chmod +x /tmp/files/*.sh",
+ "PLACEHOLDER=${var.placeholder} WIDTH=600 HEIGHT=800 PREFIX=gs /tmp/files/deploy_app.sh",
+ ]
+ }
+}
+
variable \\"base_image\\" {\\n default = \\"ubuntu-1804-bionic-v20210415\\"\\n}\\nvariable \\"project\\" {\\n default = \\"gs-test-282101\\"\\n}\\nvariable \\"region\\" {\\n default = \\"asia-northeast2\\"\\n}\\nvariable \\"zone\\" {\\n default = \\"asia-northeast2-a\\"\\n}\\nvariable \\"image_name\\" {\\n \\n}\\nvariable \\"placeholder\\" {\\n default = \\"placekitten.com\\"\\n description = \\"Image-as-a-service URL. Some other fun ones to try are fillmurray.com, placecage.com, placebeard.it, loremflickr.com, baconmockup.com, placeimg.com, placebear.com, placeskull.com, stevensegallery.com, placedog.net\\"\\n}\\n\\nsource \\"googlecompute\\" \\"basic-example\\" {\\n project_id = var.project\\n source_image = var.base_image\\n ssh_username = \\"ubuntu\\"\\n zone = var.zone\\n disk_size = 10\\n disk_type = \\"pd-ssd\\"\\n image_name = var.image_name\\n}\\n\\nbuild {\\n name = \\"packer\\"\\n source \\"sources.googlecompute.basic-example\\" {\\n name = \\"packer\\"\\n }\\n\\n provisioner \\"file\\"{\\n source = \\"./files\\"\\n destination = \\"/tmp/\\"\\n }\\n\\n provisioner \\"shell\\" {\\n inline = [\\n \\"sudo apt-get -y update\\",\\n \\"sleep 15\\",\\n \\"sudo apt-get -y update\\",\\n \\"sudo apt-get -y install apache2\\",\\n \\"sudo systemctl enable apache2\\",\\n \\"sudo systemctl start apache2\\",\\n \\"sudo chown -R ubuntu:ubuntu /var/www/html\\",\\n \\"chmod +x /tmp/files/*.sh\\",\\n \\"PLACEHOLDER=${var.placeholder} WIDTH=600 HEIGHT=800 PREFIX=gs /tmp/files/deploy_app.sh\\",\\n ]\\n }\\n}\\n
플러그인이 다양하게 준비되어 있어 빌드 작업시 스크립트는 당연하고, Ansible 같은 구성관리 코드 툴과도 조합하여 이미지를 생성하는 동작을 코드화하고 자동화 합니다.
HCP Packer가 제공하는 기능은 Packer가 생성한 이미지 Metadata에 대한 Registry 기능입니다. 개념적인 이해가 필요한 부분은 Packer로 생성되는 이미지 자체는 해당 플랫폼에 저장되며 HCP Packer는 해당 이미지에 대한 정보를 저장한다는 것으로 기존 Packer OSS와 함께 사용된다는 점입니다.
이미지 Metadata에 대한 Registry로서의 기능이 서비스로 제공된다는 것이 어떤 문제를 해결하기 위함인지에 대해 이해가 필요합니다.
기업 환경에서 표준 이미지에 대한 관리 및 관련하여 Packer를 이용하면 이미지는 자동화되어 쉽게 발생하지만 작성된 이미지를 활용하는데에 어려움이 발생합니다. 몇가지 예를 들면 다음과 같은 문제점이 있습니다.
여러 문제를 해결하기 위해 Packer에서 빌드 시 HCP Packer Registry에 Metadata를 동시에 등록하고 이미지의 속성 정보를 확인할 수 있게 되어 관리성을 높이고 외부 도구에서 명확한 이미지 ID를 쉽게 얻는 인터페이스를 제공할 수 있습니다.
HCP Packer Registry의 주요 개념은 이미지 순환(Iterations)과 이미지 채널(Channels)입니다.
Packer의 빌드마다 Iterations
에 작성된 이미지의 정보가 추가됩니다.
이렇게 추가된 Iteration 정보는 빌드시마다 기록되어 기존 Packer OSS 대비 이미지 생성에 대한 기록을 확인할 수 있습니다.
각 Iteration 항목을 클릭하면 빌드의 세부적보를 확인 할 수 있고 아래 이미지에서는 AWS와 Azure에 대한 각 멀티 클라우드, 멀티 리전에 대한 생성 정보를 확인 할 수 있습니다.
Channels
는 특정 Channel에 대해 기존 작성된 Iteration을 할당할 수 있는 객체 입니다. Channel을 통해 Terraform을 포함한 외부 툴은 Iteration의 버전을 신경쓰지 않고 원하는 Channel의 이름만 알고 있으면 항상 유효한 이미지 정보를 취득할 수 있습니다. 아래 이미지에서는 Channel을 사용자가 알기 쉬운 이름으로 구성하고 작성된 Iteration 의 버전을 맵핑하는 것을 확인 할 수 있습니다.
HCP Packer에 이미지 Metadata를 등록하는 방법은 기존 Packer로 작성된 선언의 build
블록에 hip_packer_registry
속성을 정의하는 것입니다. 관련 수행을 위한 안내는 learn.hashicorp.com의 내용을 확인할 수 있습니다.
build {
+ hcp_packer_registry {
+ bucket_name = "learn-packer-ubuntu"
+ description = <<EOT
+Some nice description about the image being published to HCP Packer Registry.
+ EOT
+ bucket_labels = {
+ "owner" = "platform-team"
+ "os" = "Ubuntu",
+ "ubuntu-version" = "Focal 20.04",
+ }
+
+ build_labels = {
+ {/* "build-time" = timestamp()
+ "build-source" = basename(path.cwd) */}
+ }
+ }
+ sources = [
+ "source.amazon-ebs.basic-example-east",
+ "source.amazon-ebs.basic-example-west"
+ ]
+}
+
현재 모든 Packer Plugin이 HCP Packer를 지원하는 것은 아니므로 Plugin 페이지에서 HCP Packer Ready
표시가 되어있는지 확인이 필요합니다. 예를들어 Docker Plugin의 페이지를 확인해보면 지원되고 있는 표시를 확인 할 수 있습니다.
기업내에서는 이미지에 대한 보안 규정 준수를 위해 Image의 revoke(취소)를 지원합니다. revoke된 Iteration은 관리자에 의해 완전 삭제되지 않는다면 복구하는 것도 가능합니다. 예를 들어 작성된 이미지이용을 중단하고 싶은 경우 Revoke Immediately
요청과 관련 설명을 추가할 수 있습니다.
HCP Packer의 정보는 외부 솔루션에서도 활용 가능합니다. Terraform과의 워크플로우에서 사용시에도 hcp
프로바이더가 추가되어 저장된 정보를 데이터 소스로 활용 가능합니다.
# This assumes HCP_CLIENT_ID and HCP_CLIENT_SECRET env variables are set
+provider "hcp" { }
+
+data "hcp_packer_iteration" "ubuntu" {
+ bucket_name = "learn-packer-ubuntu"
+ channel = "development"
+}
+
+data "hcp_packer_image" "ubuntu_us_west_1" {
+ bucket_name = "learn-packer-ubuntu"
+ cloud_provider = "aws"
+ iteration_id = data.hcp_packer_iteration.ubuntu.ulid
+ region = "us-west-1"
+}
+
+output "ubuntu_iteration" {
+ value = data.hcp_packer_iteration.ubuntu
+}
+
+output "ubuntu_us_west_1" {
+ value = data.hcp_packer_image.ubuntu_us_west_1
+}
+
Terraform Cloud Business를 사용하는 경우 HCP Packer에서 제공하는 Terraform Cloud Run Tasks
기능과 통합시킬 수 있습니다. Terraform Apply시 HCP Packer에서 제공하는 Run Tasks 정책이 적용되면 Plan과 Apply 단계 중간에 명확한 이미지에 대한 확인 및 오류 메시지를 발견할 수 있습니다.
HashiCorp의 제품은 설치형과 더불어 SaaS 모델로도 사용가능한 모델이 제공됩니다. 여기에는 지금까지 Terraform Cloud, HCP Vault, HCP Consul 이 제공되었습니다. HCP는 HashiCorp Cloud Platform의 약자 입니다.
\\n여기에 최근 HCP Packer가 공식적으로 GA(General Available)되었습니다. HashiCorp의 솔루션들에 대해서 우선 OSS(Open Source Software)로 떠올려 볼 수 있지만 기업을 위해 기능이 차별화된 설치형 엔터프라이즈와 더불어 클라우드형 서비스도 제공되고 있으며 향후 새로운 솔루션들이 추가될 전망입니다.
"}');export{V as comp,M as data}; diff --git a/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 b/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 new file mode 100644 index 0000000000..0acaaff03d Binary files /dev/null and b/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 differ diff --git a/assets/KaTeX_AMS-Regular-DMm9YOAa.woff b/assets/KaTeX_AMS-Regular-DMm9YOAa.woff new file mode 100644 index 0000000000..b804d7b33a Binary files /dev/null and b/assets/KaTeX_AMS-Regular-DMm9YOAa.woff differ diff --git a/assets/KaTeX_AMS-Regular-DRggAlZN.ttf b/assets/KaTeX_AMS-Regular-DRggAlZN.ttf new file mode 100644 index 0000000000..c6f9a5e7c0 Binary files /dev/null and b/assets/KaTeX_AMS-Regular-DRggAlZN.ttf differ diff --git a/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf b/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf new file mode 100644 index 0000000000..9ff4a5e044 Binary files /dev/null and b/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf differ diff --git a/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff b/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff new file mode 100644 index 0000000000..9759710d1d Binary files /dev/null and b/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff differ diff --git a/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 b/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 new file mode 100644 index 0000000000..f390922ece Binary files /dev/null and b/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 differ diff --git a/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff b/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff new file mode 100644 index 0000000000..9bdd534fd2 Binary files /dev/null and b/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff differ diff --git a/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 b/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 new file mode 100644 index 0000000000..75344a1f98 Binary files /dev/null and b/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 differ diff --git a/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf b/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf new file mode 100644 index 0000000000..f522294ff0 Binary files /dev/null and b/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf differ diff --git a/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf b/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf new file mode 100644 index 0000000000..4e98259c3b Binary files /dev/null and b/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf differ diff --git a/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff b/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff new file mode 100644 index 0000000000..e7730f6627 Binary files /dev/null and b/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff differ diff --git a/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 b/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 new file mode 100644 index 0000000000..395f28beac Binary files /dev/null and b/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 differ diff --git a/assets/KaTeX_Fraktur-Regular-CB_wures.ttf b/assets/KaTeX_Fraktur-Regular-CB_wures.ttf new file mode 100644 index 0000000000..b8461b275f Binary files /dev/null and b/assets/KaTeX_Fraktur-Regular-CB_wures.ttf differ diff --git a/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 b/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 new file mode 100644 index 0000000000..735f6948d6 Binary files /dev/null and b/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 differ diff --git a/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff b/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff new file mode 100644 index 0000000000..acab069f90 Binary files /dev/null and b/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff differ diff --git a/assets/KaTeX_Main-Bold-Cx986IdX.woff2 b/assets/KaTeX_Main-Bold-Cx986IdX.woff2 new file mode 100644 index 0000000000..ab2ad21da6 Binary files /dev/null and b/assets/KaTeX_Main-Bold-Cx986IdX.woff2 differ diff --git a/assets/KaTeX_Main-Bold-Jm3AIy58.woff b/assets/KaTeX_Main-Bold-Jm3AIy58.woff new file mode 100644 index 0000000000..f38136ac1c Binary files /dev/null and b/assets/KaTeX_Main-Bold-Jm3AIy58.woff differ diff --git a/assets/KaTeX_Main-Bold-waoOVXN0.ttf b/assets/KaTeX_Main-Bold-waoOVXN0.ttf new file mode 100644 index 0000000000..4060e627dc Binary files /dev/null and b/assets/KaTeX_Main-Bold-waoOVXN0.ttf differ diff --git a/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 b/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 new file mode 100644 index 0000000000..5931794de4 Binary files /dev/null and b/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 differ diff --git a/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf b/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf new file mode 100644 index 0000000000..dc007977ee Binary files /dev/null and b/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf differ diff --git a/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff b/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff new file mode 100644 index 0000000000..67807b0bd4 Binary files /dev/null and b/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff differ diff --git a/assets/KaTeX_Main-Italic-3WenGoN9.ttf b/assets/KaTeX_Main-Italic-3WenGoN9.ttf new file mode 100644 index 0000000000..0e9b0f354a Binary files /dev/null and b/assets/KaTeX_Main-Italic-3WenGoN9.ttf differ diff --git a/assets/KaTeX_Main-Italic-BMLOBm91.woff b/assets/KaTeX_Main-Italic-BMLOBm91.woff new file mode 100644 index 0000000000..6f43b594b6 Binary files /dev/null and b/assets/KaTeX_Main-Italic-BMLOBm91.woff differ diff --git a/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 b/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 new file mode 100644 index 0000000000..b50920e138 Binary files /dev/null and b/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 differ diff --git a/assets/KaTeX_Main-Regular-B22Nviop.woff2 b/assets/KaTeX_Main-Regular-B22Nviop.woff2 new file mode 100644 index 0000000000..eb24a7ba28 Binary files /dev/null and b/assets/KaTeX_Main-Regular-B22Nviop.woff2 differ diff --git a/assets/KaTeX_Main-Regular-Dr94JaBh.woff b/assets/KaTeX_Main-Regular-Dr94JaBh.woff new file mode 100644 index 0000000000..21f5812968 Binary files /dev/null and b/assets/KaTeX_Main-Regular-Dr94JaBh.woff differ diff --git a/assets/KaTeX_Main-Regular-ypZvNtVU.ttf b/assets/KaTeX_Main-Regular-ypZvNtVU.ttf new file mode 100644 index 0000000000..dd45e1ed2e Binary files /dev/null and b/assets/KaTeX_Main-Regular-ypZvNtVU.ttf differ diff --git a/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf b/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf new file mode 100644 index 0000000000..728ce7a1e2 Binary files /dev/null and b/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf differ diff --git a/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 b/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 new file mode 100644 index 0000000000..29657023ad Binary files /dev/null and b/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 differ diff --git a/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff b/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff new file mode 100644 index 0000000000..0ae390d74c Binary files /dev/null and b/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff differ diff --git a/assets/KaTeX_Math-Italic-DA0__PXp.woff b/assets/KaTeX_Math-Italic-DA0__PXp.woff new file mode 100644 index 0000000000..eb5159d4c1 Binary files /dev/null and b/assets/KaTeX_Math-Italic-DA0__PXp.woff differ diff --git a/assets/KaTeX_Math-Italic-flOr_0UB.ttf b/assets/KaTeX_Math-Italic-flOr_0UB.ttf new file mode 100644 index 0000000000..70d559b4e9 Binary files /dev/null and b/assets/KaTeX_Math-Italic-flOr_0UB.ttf differ diff --git a/assets/KaTeX_Math-Italic-t53AETM-.woff2 b/assets/KaTeX_Math-Italic-t53AETM-.woff2 new file mode 100644 index 0000000000..215c143fd7 Binary files /dev/null and b/assets/KaTeX_Math-Italic-t53AETM-.woff2 differ diff --git a/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf b/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf new file mode 100644 index 0000000000..2f65a8a3a6 Binary files /dev/null and b/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf differ diff --git a/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 b/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 new file mode 100644 index 0000000000..cfaa3bda59 Binary files /dev/null and b/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 differ diff --git a/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff b/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff new file mode 100644 index 0000000000..8d47c02d94 Binary files /dev/null and b/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff differ diff --git a/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 b/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 new file mode 100644 index 0000000000..349c06dc60 Binary files /dev/null and b/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 differ diff --git a/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff b/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff new file mode 100644 index 0000000000..7e02df9636 Binary files /dev/null and b/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff differ diff --git a/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf b/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf new file mode 100644 index 0000000000..d5850df98e Binary files /dev/null and b/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf differ diff --git a/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf b/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf new file mode 100644 index 0000000000..537279f6bd Binary files /dev/null and b/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf differ diff --git a/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff b/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff new file mode 100644 index 0000000000..31b84829b4 Binary files /dev/null and b/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff differ diff --git a/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 b/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 new file mode 100644 index 0000000000..a90eea85f6 Binary files /dev/null and b/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 differ diff --git a/assets/KaTeX_Script-Regular-C5JkGWo-.ttf b/assets/KaTeX_Script-Regular-C5JkGWo-.ttf new file mode 100644 index 0000000000..fd679bf374 Binary files /dev/null and b/assets/KaTeX_Script-Regular-C5JkGWo-.ttf differ diff --git a/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 b/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 new file mode 100644 index 0000000000..b3048fc115 Binary files /dev/null and b/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 differ diff --git a/assets/KaTeX_Script-Regular-D5yQViql.woff b/assets/KaTeX_Script-Regular-D5yQViql.woff new file mode 100644 index 0000000000..0e7da821ee Binary files /dev/null and b/assets/KaTeX_Script-Regular-D5yQViql.woff differ diff --git a/assets/KaTeX_Size1-Regular-C195tn64.woff b/assets/KaTeX_Size1-Regular-C195tn64.woff new file mode 100644 index 0000000000..7f292d9118 Binary files /dev/null and b/assets/KaTeX_Size1-Regular-C195tn64.woff differ diff --git a/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf b/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf new file mode 100644 index 0000000000..871fd7d19d Binary files /dev/null and b/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf differ diff --git a/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 b/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 new file mode 100644 index 0000000000..c5a8462fbf Binary files /dev/null and b/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 differ diff --git a/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf b/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf new file mode 100644 index 0000000000..7a212caf91 Binary files /dev/null and b/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf differ diff --git a/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 b/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 new file mode 100644 index 0000000000..e1bccfe240 Binary files /dev/null and b/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 differ diff --git a/assets/KaTeX_Size2-Regular-oD1tc_U0.woff b/assets/KaTeX_Size2-Regular-oD1tc_U0.woff new file mode 100644 index 0000000000..d241d9be2d Binary files /dev/null and b/assets/KaTeX_Size2-Regular-oD1tc_U0.woff differ diff --git a/assets/KaTeX_Size3-Regular-CTq5MqoE.woff b/assets/KaTeX_Size3-Regular-CTq5MqoE.woff new file mode 100644 index 0000000000..e6e9b658dc Binary files /dev/null and b/assets/KaTeX_Size3-Regular-CTq5MqoE.woff differ diff --git a/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf b/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf new file mode 100644 index 0000000000..00bff3495f Binary files /dev/null and b/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf differ diff --git a/assets/KaTeX_Size4-Regular-BF-4gkZK.woff b/assets/KaTeX_Size4-Regular-BF-4gkZK.woff new file mode 100644 index 0000000000..e1ec545766 Binary files /dev/null and b/assets/KaTeX_Size4-Regular-BF-4gkZK.woff differ diff --git a/assets/KaTeX_Size4-Regular-DWFBv043.ttf b/assets/KaTeX_Size4-Regular-DWFBv043.ttf new file mode 100644 index 0000000000..74f08921f0 Binary files /dev/null and b/assets/KaTeX_Size4-Regular-DWFBv043.ttf differ diff --git a/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 b/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 new file mode 100644 index 0000000000..680c130850 Binary files /dev/null and b/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 differ diff --git a/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff b/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff new file mode 100644 index 0000000000..2432419f28 Binary files /dev/null and b/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff differ diff --git a/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 b/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 new file mode 100644 index 0000000000..771f1af705 Binary files /dev/null and b/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 differ diff --git a/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf b/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf new file mode 100644 index 0000000000..c83252c571 Binary files /dev/null and b/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf differ diff --git a/assets/Keyboard-Eng.html-CK_we2gb.js b/assets/Keyboard-Eng.html-CK_we2gb.js new file mode 100644 index 0000000000..59434f76be --- /dev/null +++ b/assets/Keyboard-Eng.html-CK_we2gb.js @@ -0,0 +1 @@ +import{_ as d}from"./plugin-vue_export-helper-DlAUqK2U.js";import{r as o,o as a,c as n,b as t,d as e,a as c,e as i}from"./app-Bzk8Nrll.js";const s={},p=t("h1",{id:"키보드의-특수기호-영어-명칭",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#키보드의-특수기호-영어-명칭"},[t("span",null,"키보드의 특수기호 영어 명칭")])],-1),l={href:"https://www.facebook.com/DoppioLover/posts/10225430970749092?__cft__%5B0%5D=AZVQVje3HpQ-XcR1aulTomjrKkwP3dgMkwtxvqSrRed0-yZn5vMd_fFoawk9FqeqSj7bIGYg4Ui1zUDYaE2anZDkndlmTgjhFCEnIkTD__lyGfjDt8Kf8od2Ayz3ZPT4PSo&__tn__=%2CO%2CP-R",target:"_blank",rel:"noopener noreferrer"},m=i("기호 | 영어 명칭 |
---|---|
~ | tilde. 이건 미국에서는 "틸다" 쯤 발음한다. 그런데 이게 뭐라 부르는지 모르는 분들 꽤 많음. 그냥 wavy thingy under escape하는 사람도 본 적이 있다. |
` | backtick 또는 backquote |
! | exclamation 또는 exclamation point. IT업계 종사자는 bang이라고도 한다. |
@ | at sign |
# | pound 또는 crosshatch 또는 number sign 또는 hash. 한국에서는 흔히 sharp라고 불리는데 의외로 미국에서는 그렇게 부르는 사람을 거의 못봤다. 아마도 한국에서는 음악교육에서 악보일기를 가르쳐서 그런 것 아닐까 싶다. |
$ | dollar sign |
% | percent sign |
^ | caret 또는 circumflex. 과학용 계산기에 익숙한 사람들은 exponential power sign이라고 하기도 한다. 그런데 미국사람들도 이걸 뭐라는지 모르는 사람이 의외로 많다. 그냥 6키 위에 있는 것(that thing above 6 key)라고 하는 사람들 간혹 만난다. |
& | ampersand 또는 and sign |
* | asterisk 또는 star symbol. |
( | opening parenthesis. 줄여서 open paren만 하는 경우가 흔하다. |
) | closing parenthesis. 줄여서 close paren... |
_ | underscore. 아주 드물게 underbar라고 하는 사람도 있다. |
- | minus sign 또는 hyphen 또는 dash |
+ | plus sign |
= | equal sign |
{ | opening brace . 흔히 left curly brace라고불린다. |
} | closing brace 또는 right curly brace |
[ | opening bracket. 흔히 left square bracket이라고도 불린다. |
] | closing bracket 또는 right square bracket |
| | pipe 또는 vertical bar |
\\ | back slash 또는 backward slash |
: | colon |
; | semicolon 발음은 세미콜론 또는 세마이콜론 |
“ | quotation mark 또는 double quote |
‘ | apostrophe 또는 single quote |
< | less than sign. 가끔 left angle bracket이라고 부르는 경우가 있다. |
, | comma |
> | greater than sign. 가끔 right angle bracket이라고 하는 사람들이 있다. |
. | period 또는 dot. 일반적으로 period는 영어 언어적 용도로, dot은 컴퓨터 프로그래밍 관련으로 쓰인다. |
? | question mark |
/ | slash 또는 forward slash. |
\\n\\n"}');export{y as comp,f as data}; diff --git a/assets/Kubernetes_scheduler.html-DMN8z5Cq.js b/assets/Kubernetes_scheduler.html-DMN8z5Cq.js new file mode 100644 index 0000000000..e9b3a4f977 --- /dev/null +++ b/assets/Kubernetes_scheduler.html-DMN8z5Cq.js @@ -0,0 +1,3 @@ +import{_ as l}from"./plugin-vue_export-helper-DlAUqK2U.js";import{r as n,o as i,c as s,b as e,d as t,a as o,e as a}from"./app-Bzk8Nrll.js";const u={},c=e("h1",{id:"kubernetes-스케쥴러-알고리즘",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#kubernetes-스케쥴러-알고리즘"},[e("span",null,"kubernetes 스케쥴러 알고리즘")])],-1),d={href:"https://github.com/kubernetes/community/blob/master/contributors/devel/sig-scheduling/scheduler_algorithm.md",target:"_blank",rel:"noopener noreferrer"},h={href:"http://scheduler.md",target:"_blank",rel:"noopener noreferrer"},m=a(`
노드를 필터링하는 목적은 포드의 특정 요구 사항을 충족하지 않는 노드를 필터링하는 것입니다. 예를 들어 노드의 사용 가능한 리소스 (노드에서 이미 실행 된 모든 Pod의 리소스 요청 합계를 뺀 용량으로 측정)가 Pod의 필수 리소스보다 적은 경우 순위에서 노드를 고려해서는 안됩니다. 단계로 필터링됩니다. 현재 다음을 포함하여 서로 다른 필터링 정책을 구현하는 여러 "술어"가 있습니다.
finalScoreNodeA = (weight1 * priorityFunc1) + (weight2 * priorityFunc2)
+
+
설명하던 글의 특정 단어에 대해 외부 링크를 추가하고자 하는 경우 브라킷[ ]
과 괄호를 사용합니다. domain을 같이 기입하는 경우 새창에서 열기로 표기됩니다.
새창으로 이동하는 [링크 달기](http://docmoa.github.io/00-Howto/03-Tips/Link.html)
+현재 창에서 이동하는 [링크 달기](/00-Howto/03-Tips/Link.html)
+
다음과 같이 표기됩니다.
`,4),g={href:"http://docmoa.github.io/00-Howto/03-Tips/Link.html",target:"_blank",rel:"noopener noreferrer"},f=e(`별도의 연결된 단어 없이 링크 자체를 넣는 경우 < >
를 사용합니다.
<https://docmoa.github.io>
+<docmoa@gmail.com>
+
다음과 같이 표기됩니다.
`,4),w={href:"https://docmoa.github.io",target:"_blank",rel:"noopener noreferrer"},_=a("li",null,[a("em",null,[a("strong",null,[a("a",{href:"mailto:docmoa@gmail.com"},"docmoa@gmail.com")])])],-1),v=e(`이미지는 !
를 기존 링크 문법 앞에 추가합니다.
![대체 텍스트](이미지 링크 "이미지 설명")
+
+![](https://icons.iconarchive.com/icons/fatcow/farm-fresh/32/layout-link-icon.png)
+![대체 텍스트](https://img.icons8.com/ios/2x/깨진링크)
+![대체 텍스트](https://icons.iconarchive.com/icons/fatcow/farm-fresh/32/report-link-icon.png "이미지 설명")
+
다음과 같이 표기됩니다.
공유
버튼을 누르고 퍼가기
를 클릭하면 우측에 동영상 퍼가기 코드가 나타납니다.<iframe width="560" height="315" src="https://www.youtube.com/embed/StTqXEQ2l-Y" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
+
다음과 같이 표기됩니다.
`,3),x=a("iframe",{width:"560",height:"315",src:"https://www.youtube.com/embed/StTqXEQ2l-Y",title:"YouTube video player",frameborder:"0",allow:"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture",allowfullscreen:""},null,-1);function q(L,T){const s=o("ExternalLinkIcon"),c=o("RouteLink");return l(),p("div",null,[m,a("p",null,[n("문서 작성시 외부 링크를 포함하는 예를 설명합니다. "),a("a",h,[n("참고 문서"),t(s)])]),k,a("ul",null,[a("li",null,[n("새창으로 이동하는 "),a("a",g,[n("링크 달기"),t(s)])]),a("li",null,[n("현재 창에서 이동하는 "),t(c,{to:"/00-Howto/03-Tips/Link.html"},{default:r(()=>[n("링크 달기")]),_:1})])]),f,a("ul",null,[a("li",null,[a("em",null,[a("strong",null,[a("a",w,[n("https://docmoa.github.io"),t(s)])])])]),_]),v,a("p",null,[n("외부 동영상은 html 문법을 활용하여 추가할 수 있습니다. 여기서는 유튜브를 예를 들어 설명합니다. 다른 여러가지 방식은 "),a("a",b,[n("참고 링크"),t(s)]),n("를 확인해주세요.")]),y,x])}const z=i(d,[["render",q],["__file","Link.html.vue"]]),E=JSON.parse('{"path":"/00-Howto/03-Tips/Link.html","title":"Link","lang":"ko-KR","frontmatter":{"description":"Link 문서 작성시 외부 링크를 포함하는 예를 설명합니다. 참고 문서 텍스트에 링크 달기 설명하던 글의 특정 단어에 대해 외부 링크를 추가하고자 하는 경우 브라킷[ ]과 괄호를 사용합니다. domain을 같이 기입하는 경우 새창에서 열기로 표기됩니다. 다음과 같이 표기됩니다. 새창으로 이동하는 링크 달기 현재 창에...","head":[["meta",{"property":"og:url","content":"https://docmoa.github.io/00-Howto/03-Tips/Link.html"}],["meta",{"property":"og:site_name","content":"docmoa"}],["meta",{"property":"og:title","content":"Link"}],["meta",{"property":"og:description","content":"Link 문서 작성시 외부 링크를 포함하는 예를 설명합니다. 참고 문서 텍스트에 링크 달기 설명하던 글의 특정 단어에 대해 외부 링크를 추가하고자 하는 경우 브라킷[ ]과 괄호를 사용합니다. domain을 같이 기입하는 경우 새창에서 열기로 표기됩니다. 다음과 같이 표기됩니다. 새창으로 이동하는 링크 달기 현재 창에..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:image","content":"https://icons.iconarchive.com/icons/fatcow/farm-fresh/32/layout-link-icon.png"}],["meta",{"property":"og:locale","content":"ko-KR"}],["meta",{"property":"og:updated_time","content":"2023-09-18T13:12:54.000Z"}],["meta",{"name":"twitter:card","content":"summary_large_image"}],["meta",{"name":"twitter:image:alt","content":"Link"}],["meta",{"property":"article:modified_time","content":"2023-09-18T13:12:54.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Link\\",\\"image\\":[\\"https://icons.iconarchive.com/icons/fatcow/farm-fresh/32/layout-link-icon.png\\",\\"https://img.icons8.com/ios/2x/깨진링크\\",\\"https://icons.iconarchive.com/icons/fatcow/farm-fresh/32/report-link-icon.png \\\\\\"이미지 설명\\\\\\"\\",\\"https://icons.iconarchive.com/icons/fatcow/farm-fresh/32/layout-link-icon.png\\",\\"https://img.icons8.com/ios/2x/깨진링크\\",\\"https://icons.iconarchive.com/icons/fatcow/farm-fresh/32/report-link-icon.png \\\\\\"이미지 설명\\\\\\"\\"],\\"dateModified\\":\\"2023-09-18T13:12:54.000Z\\",\\"author\\":[]}"]]},"headers":[{"level":2,"title":"텍스트에 링크 달기","slug":"텍스트에-링크-달기","link":"#텍스트에-링크-달기","children":[]},{"level":2,"title":"링크 자체를 표기","slug":"링크-자체를-표기","link":"#링크-자체를-표기","children":[]},{"level":2,"title":"이미지","slug":"이미지","link":"#이미지","children":[]},{"level":2,"title":"동영상","slug":"동영상","link":"#동영상","children":[]}],"git":{"createdTime":1634225909000,"updatedTime":1695042774000,"contributors":[{"name":"Administrator","email":"admin@example.com","commits":1},{"name":"Great-Stone","email":"hahohh@gmail.com","commits":1}]},"readingTime":{"minutes":0.44,"words":133},"filePathRelative":"00-Howto/03-Tips/Link.md","localizedDate":"2021년 10월 15일","excerpt":"\\n문서 작성시 외부 링크를 포함하는 예를 설명합니다. 참고 문서
\\n설명하던 글의 특정 단어에 대해 외부 링크를 추가하고자 하는 경우 브라킷[ ]
과 괄호를 사용합니다. domain을 같이 기입하는 경우 새창에서 열기로 표기됩니다.
새창으로 이동하는 [링크 달기](http://docmoa.github.io/00-Howto/03-Tips/Link.html)\\n현재 창에서 이동하는 [링크 달기](/00-Howto/03-Tips/Link.html)\\n
$ nomad namespace apply -description "PoC Application" apps
+
$ nomad namespace delete apps
+
$ nomad namespace list
+Name Description
+default Default shared namespace
+
job "rails-www" {
+
+ ## Run in the QA environments
+ namespace = "web-qa"
+
+ ## Only run in one datacenter when QAing
+ datacenters = ["us-west1"]
+ # ...
+}
+
# flag 설정
+nomad job status -namespace=web-qa
+
+# ENV 설정
+export NOMAD_NAMESPACE=web-qa
+nomad job status
+
# Allow read only access to the production namespace
+namespace "web-prod" {
+ policy = "read"
+}
+
+# Allow writing to the QA namespace
+namespace "web-qa" {
+ policy = "write"
+}
+
\\n\\nNomad Version : >= 1.0.0
\\n
\\nNomad Ent. Version : >= 0.7.0
\\nhttps://learn.hashicorp.com/tutorials/nomad/namespaces
$ nomad namespace apply -description \\"PoC Application\\" apps\\n
팁
해당 Token의 policy는 특정인이 원하여 만들었으며, 더 다양한 제약과 허용을 할 수 있습니다. 해당 policy는 아래와 같은 제약과 허용을 합니다.
#원하는 권한이 있는 policy file
+$ cat nomad-ui-policy.hcl
+namespace "*" {
+ policy = "read"
+ capabilities = ["submit-job", "dispatch-job", "read-logs", "list-jobs", "parse-job", "read-job", "csi-list-volume", "csi-read-volume", "list-scaling-policies", "read-scaling-policy", "read-job-scaling", "read-fs"]
+}
+node {
+ policy = "read"
+}
+host_volume "*" {
+ policy = "write"
+}
+plugin {
+ policy = "read"
+}
+
+#위에서 만든 policy 파일을 nomad cluster에 적용
+$ nomad acl policy apply -description "Production UI policy" prod-ui nomad-ui-policy.hcl
+
+#해당 policy로 token생성(policy는 여러개를 넣을 수도 있음)
+$ nomad acl token create -name="prod ui token" -policy=prod-ui -type=client | tee ui-prod.token
+#웹 브라우저 로그인을 위해 Secret ID 복사
+
아래는 위에서 만들어진 토큰으로 로그인한 화면입니다.
아래 그림과 같이 exec버튼이 비활성화되어 있는 걸 볼 수 있습니다.
팁
\\n해당 Token의 policy는 특정인이 원하여 만들었으며, 더 다양한 제약과 허용을 할 수 있습니다. 해당 policy는 아래와 같은 제약과 허용을 합니다.
\\n#원하는 권한이 있는 policy file\\n$ cat nomad-ui-policy.hcl\\nnamespace \\"*\\" {\\n policy = \\"read\\"\\n capabilities = [\\"submit-job\\", \\"dispatch-job\\", \\"read-logs\\", \\"list-jobs\\", \\"parse-job\\", \\"read-job\\", \\"csi-list-volume\\", \\"csi-read-volume\\", \\"list-scaling-policies\\", \\"read-scaling-policy\\", \\"read-job-scaling\\", \\"read-fs\\"]\\n}\\nnode {\\n policy = \\"read\\"\\n}\\nhost_volume \\"*\\" {\\n policy = \\"write\\"\\n}\\nplugin {\\n policy = \\"read\\"\\n}\\n\\n#위에서 만든 policy 파일을 nomad cluster에 적용\\n$ nomad acl policy apply -description \\"Production UI policy\\" prod-ui nomad-ui-policy.hcl\\n\\n#해당 policy로 token생성(policy는 여러개를 넣을 수도 있음)\\n$ nomad acl token create -name=\\"prod ui token\\" -policy=prod-ui -type=client | tee ui-prod.token\\n#웹 브라우저 로그인을 위해 Secret ID 복사\\n
팁
공식 사이트에 consul 인증서 생성 가이드는 있는데 Nomad 인증서 생성가이드는
Show Terminal을 들어가야 볼 수 있기때문에 귀찮음을 해결하기 위해 공유합니다.
consul tls ca create -domain=nomad -days 3650
+
+consul tls cert create -domain=nomad -dc=global -server -days 3650
+
+consul tls cert create -domain=nomad -dc=global -client -days 3650
+
+consul tls cert create -domain=nomad -dc=global -cli -days 3650
+
export NOMAD_CACERT="\${HOME}/tls/nomad-agent-ca.pem"
+
+export NOMAD_CLIENT_CERT="\${HOME}/tls/global-cli-nomad-0.pem"
+
+export NOMAD_CLIENT_KEY="\${HOME}/tls/global-cli-nomad-0-key.pem"
+
+export NOMAD_ADDR="https://127.0.0.1:4646"
+
팁
\\n공식 사이트에 consul 인증서 생성 가이드는 있는데 Nomad 인증서 생성가이드는
\\nShow Terminal을 들어가야 볼 수 있기때문에 귀찮음을 해결하기 위해 공유합니다.
consul tls ca create -domain=nomad -days 3650\\n\\nconsul tls cert create -domain=nomad -dc=global -server -days 3650\\n\\nconsul tls cert create -domain=nomad -dc=global -client -days 3650\\n\\nconsul tls cert create -domain=nomad -dc=global -cli -days 3650\\n
Log |
---|
Error : compute.VirtualMachinesClient#CreateOrUpdate: Failure sending request: StatusCode=400 – Original Error: Code=“InvalidParameter” Message=“The Admin Username specified is not allowed.” Target="adminUsername" |
Azure(azurerm) 프로바이더를 사용하여 Virtual Machine을 프로비저닝하는 경우 OSProfile
에서 Admin User Name을 잘못된 조건으로 구성하는 경우 발생 할 수 있음
Azure의 API에서 정의하는 OSProfile
내의 AdminUsername
은 온라인 문서에서처럼 몇가지 룰이 있다.
.
으로 끝날 수 없음Log | \\n
---|
Error : compute.VirtualMachinesClient#CreateOrUpdate: Failure sending request: StatusCode=400 – Original Error: Code=“InvalidParameter” Message=“The Admin Username specified is not allowed.” Target=\\"adminUsername\\" | \\n
nomad namespace apply -description "Boundary" boundary
+
job "postgresql" {
+ type = "service"
+ datacenters = ["hashistack"]
+ namespace = "boundary"
+
+ group "postgres" {
+ count = 1
+
+ volume "postgres-vol" {
+ type = "host"
+ read_only = false
+ source = "postgres-vol"
+ }
+
+ task "db" {
+ driver = "docker"
+
+ volume_mount {
+ volume = "postgres-vol"
+ destination = "/var/lib/postgresql/data"
+ read_only = false
+ }
+
+ config {
+ image = "postgres:13.2"
+ port_map {
+ pg = 5432
+ }
+ }
+
+ env {
+ POSTGRES_PASSWORD = "postgres"
+ POSTGRES_USER = "postgres"
+ PGDATA = "/var/lib/postgresql/data/pgdata"
+ }
+
+ resources {
+ memory = 1024
+
+ network {
+ port "pg" {
+ static = 5432
+ }
+ }
+ }
+
+ service {
+ name = "postgresql"
+ tags = ["db", "boundary"]
+
+ port = "pg"
+
+ check {
+ type = "tcp"
+ interval = "10s"
+ timeout = "2s"
+ port = "pg"
+ }
+ }
+ }
+ }
+}
+
nomad job run -namespace="boundary" postgresql.nomad
+
# Login
+psql -h 172.28.128.11 -U postgres postgres
+
# <enter the password> postgres
+CREATE ROLE boundary WITH LOGIN PASSWORD 'PASSWORD';
+CREATE DATABASE boundary OWNER boundary;
+GRANT ALL PRIVILEGES ON DATABASE boundary TO boundary;
+ALTER USER boundary PASSWORD 'boundary';
+
</tmp/config.hcl>
disable_mlock = true
+
+controller {
+ name = "controller-0"
+ database {
+ url = "postgresql://boundary:boundary@172.28.128.11:5432/boundary?sslmode=disable"
+ }
+}
+
+kms "aead" {
+ purpose = "root"
+ aead_type = "aes-gcm"
+ key = "sP1fnF5Xz85RrXyELHFeZg9Ad2qt4Z4bgNHVGtD6ung="
+ key_id = "global_root"
+}
+
+kms "aead" {
+ purpose = "worker-auth"
+ aead_type = "aes-gcm"
+ key = "8fZBjCUfN0TzjEGLQldGY4+iE9AkOvCfjh7+p0GtRBQ="
+ key_id = "global_worker-auth"
+}
+
+kms "aead" {
+ purpose = "recovery"
+ aead_type = "aes-gcm"
+ key = "8fZBjCUfN0TzjEGLQldGY4+iE9AkOvCfjh7+p0GtRBQ="
+ key_id = "global_recovery"
+}
+
boundary database init -config=/tmp/config.hcl
+
locals {
+ version = "0.6.2"
+ postgre_ip = "172.28.128.11"
+ postgre_port = "5432"
+}
+
+job "boundary-controller" {
+ type = "service"
+ datacenters = ["hashistack"]
+ namespace = "boundary"
+
+ group "controller" {
+ count = 1
+
+ network {
+ mode = "host"
+ }
+
+ task "migration" {
+ driver = "raw_exec"
+
+ env {
+ BOUNDARY_POSTGRES_URL = "postgresql://boundary:boundary@${local.postgre_ip}:${local.postgre_port}/boundary?sslmode=disable"
+ }
+ artifact {
+ source = "https://releases.hashicorp.com/boundary/${local.version}/boundary_${local.version}_linux_amd64.zip"
+ }
+ template {
+ data = <<EOH
+disable_mlock = true
+
+{{ range service "postgresql" }}
+controller {
+ name = "controller-0"
+ database {
+ url = "postgresql://boundary:boundary@{{ .Address }}:{{ .Port }}/boundary?sslmode=disable"
+ }
+}
+{{ end }}
+
+listener "tcp" {
+ address = "0.0.0.0:9200"
+ purpose = "api"
+ tls_disable = true
+}
+listener "tcp" {
+ address = "0.0.0.0:9201"
+ purpose = "cluster"
+ tls_disable = true
+}
+
+kms "aead" {
+ purpose = "root"
+ aead_type = "aes-gcm"
+ key = "sP1fnF5Xz85RrXyELHFeZg9Ad2qt4Z4bgNHVGtD6ung="
+ key_id = "global_root"
+}
+
+kms "aead" {
+ purpose = "worker-auth"
+ aead_type = "aes-gcm"
+ key = "8fZBjCUfN0TzjEGLQldGY4+iE9AkOvCfjh7+p0GtRBQ="
+ key_id = "global_worker-auth"
+}
+
+kms "aead" {
+ purpose = "recovery"
+ aead_type = "aes-gcm"
+ key = "8fZBjCUfN0TzjEGLQldGY4+iE9AkOvCfjh7+p0GtRBQ="
+ key_id = "global_recovery"
+}
+EOH
+ destination = "local/config/config.hcl"
+ }
+ config {
+ command = "local/boundary"
+ args = ["database", "migrate", "-config", "local/config/config.hcl"]
+ }
+ lifecycle {
+ hook = "prestart"
+ sidecar = false
+ }
+ }
+
+ task "controller" {
+ driver = "docker"
+
+ config {
+ image = "hashicorp/boundary:${local.version}"
+ port_map {
+ controller = 9200
+ cluster = 9201
+ }
+ mount {
+ type = "bind"
+ source = "local/config"
+ target = "/boundary"
+ }
+ // network_mode = "boundary-net"
+ // network_aliases = [
+ // "boundary-controller"
+ // ]
+ }
+
+ template {
+ data = <<EOH
+disable_mlock = true
+
+{{ range service "postgresql" }}
+controller {
+ name = "controller-0"
+ database {
+ url = "postgresql://boundary:boundary@{{ .Address }}:{{ .Port }}/boundary?sslmode=disable"
+ }
+ public_cluster_addr = "{{ env "NOMAD_ADDR_cluster" }}"
+}
+{{ end }}
+
+listener "tcp" {
+ address = "0.0.0.0:9200"
+ purpose = "api"
+ tls_disable = true
+}
+listener "tcp" {
+ address = "0.0.0.0:9201"
+ purpose = "cluster"
+ tls_disable = true
+}
+
+kms "aead" {
+ purpose = "root"
+ aead_type = "aes-gcm"
+ key = "sP1fnF5Xz85RrXyELHFeZg9Ad2qt4Z4bgNHVGtD6ung="
+ key_id = "global_root"
+}
+
+kms "aead" {
+ purpose = "worker-auth"
+ aead_type = "aes-gcm"
+ key = "8fZBjCUfN0TzjEGLQldGY4+iE9AkOvCfjh7+p0GtRBQ="
+ key_id = "global_worker-auth"
+}
+
+kms "aead" {
+ purpose = "recovery"
+ aead_type = "aes-gcm"
+ key = "8fZBjCUfN0TzjEGLQldGY4+iE9AkOvCfjh7+p0GtRBQ="
+ key_id = "global_recovery"
+}
+EOH
+ destination = "local/config/config.hcl"
+ }
+
+ env {
+ // BOUNDARY_POSTGRES_URL = "postgresql://boundary:boundary@\${local.postgre_ip}:\${local.postgre_port}/boundary?sslmode=disable"
+ SKIP_SETCAP = true
+ }
+
+ resources {
+ cpu = 300
+ memory = 500
+ network {
+ port "controller" {
+ static = 9200
+ }
+ port "cluster" {
+ static = 9201
+ }
+ }
+ }
+
+ service {
+ name = "boundary"
+ tags = ["cluster"]
+
+ port = "cluster"
+
+ check {
+ type = "tcp"
+ interval = "10s"
+ timeout = "2s"
+ port = "cluster"
+ }
+ }
+ }
+ }
+}
+
nomad job run -namespace="boundary" boundary-controller.nomad
+
locals {
+ version = "0.6.2"
+}
+
+job "boundary-worker" {
+ type = "service"
+ datacenters = ["hashistack"]
+ namespace = "boundary"
+
+ group "worker" {
+ count = 1
+
+ scaling {
+ enabled = true
+ min = 1
+ max = 3
+ }
+
+ network {
+ mode = "host"
+ }
+
+ task "worker" {
+ driver = "docker"
+
+ config {
+ image = "hashicorp/boundary:${local.version}"
+ port_map {
+ proxy = 9202
+ }
+ volumes = [
+ "local/boundary:/boundary/",
+ ]
+ // network_mode = "boundary-net"
+ }
+
+ template {
+ data = <<EOH
+disable_mlock = true
+
+listener "tcp" {
+ address = "0.0.0.0:9202"
+ purpose = "proxy"
+ tls_disable = true
+}
+
+worker {
+ name = "worker-0"
+ controllers = [
+{{ range service "boundary" }}
+ "{{ .Address }}",
+{{ end }}
+ ]
+ public_addr = "{{ env "NOMAD_ADDR_proxy" }}"
+}
+
+kms "aead" {
+ purpose = "root"
+ aead_type = "aes-gcm"
+ key = "sP1fnF5Xz85RrXyELHFeZg9Ad2qt4Z4bgNHVGtD6ung="
+ key_id = "global_root"
+}
+
+kms "aead" {
+ purpose = "worker-auth"
+ aead_type = "aes-gcm"
+ key = "8fZBjCUfN0TzjEGLQldGY4+iE9AkOvCfjh7+p0GtRBQ="
+ key_id = "global_worker-auth"
+}
+
+kms "aead" {
+ purpose = "recovery"
+ aead_type = "aes-gcm"
+ key = "8fZBjCUfN0TzjEGLQldGY4+iE9AkOvCfjh7+p0GtRBQ="
+ key_id = "global_recovery"
+}
+EOH
+ destination = "/local/boundary/config.hcl"
+ }
+
+ env {
+ // BOUNDARY_POSTGRES_URL = "postgresql://boundary:boundary@172.28.128.11:5432/boundary?sslmode=disable"
+ SKIP_SETCAP = true
+ }
+
+ resources {
+ network {
+ port "proxy" {}
+ }
+ }
+ }
+ }
+}
+
nomad job run -namespace="boundary" boundary-worker.nomad
+
nomad namespace apply -description \\"Boundary\\" boundary\\n
locals {
+ version = "0.8.1"
+ private_ip = "192.168.0.27"
+ public_ip = "11.129.13.30"
+}
+
+job "boundary-dev" {
+ type = "service"
+ datacenters = ["home"]
+ namespace = "boundary"
+
+ constraint {
+ attribute = "${attr.os.name}"
+ value = "raspbian"
+ }
+
+ group "dev" {
+ count = 1
+
+ ephemeral_disk { sticky = true }
+
+ network {
+ mode = "host"
+ port "api" {
+ static = 9200
+ to = 9200
+ }
+ port "cluster" {
+ static = 9201
+ to = 9201
+ }
+ port "worker" {
+ static = 9202
+ to = 9202
+ }
+ }
+
+ task "dev" {
+ driver = "raw_exec"
+
+ env {
+ BOUNDARY_DEV_CONTROLLER_API_LISTEN_ADDRESS = local.private_ip
+ BOUNDARY_DEV_CONTROLLER_CLUSTER_LISTEN_ADDRESS = "0.0.0.0"
+ BOUNDARY_DEV_WORKER_PUBLIC_ADDRESS = local.public_ip
+ BOUNDARY_DEV_WORKER_PROXY_LISTEN_ADDRESS = local.private_ip
+ BOUNDARY_DEV_PASSWORD = "password"
+ }
+
+ // artifact {
+ // source = "https://releases.hashicorp.com/boundary/\${local.version}/boundary_\${local.version}_linux_arm.zip"
+ // }
+
+ config {
+ command = "boundary"
+ args = ["dev"]
+ }
+
+ resources {
+ cpu = 500
+ memory = 500
+ }
+
+ service {
+ name = "boundary"
+ tags = ["cluster"]
+
+ port = "cluster"
+
+ check {
+ type = "tcp"
+ interval = "10s"
+ timeout = "2s"
+ port = "api"
+ }
+ }
+ }
+ }
+}
+
-worker-public-addr
flag)locals {\\n version = \\"0.8.1\\"\\n private_ip = \\"192.168.0.27\\"\\n public_ip = \\"11.129.13.30\\"\\n}\\n\\njob \\"boundary-dev\\" {\\n type = \\"service\\"\\n datacenters = [\\"home\\"]\\n namespace = \\"boundary\\"\\n\\n constraint {\\n attribute = \\"${attr.os.name}\\"\\n value = \\"raspbian\\"\\n }\\n\\n group \\"dev\\" {\\n count = 1\\n\\n ephemeral_disk { sticky = true }\\n\\n network {\\n mode = \\"host\\"\\n port \\"api\\" {\\n static = 9200\\n to = 9200\\n }\\n port \\"cluster\\" {\\n static = 9201\\n to = 9201\\n }\\n port \\"worker\\" {\\n static = 9202\\n to = 9202\\n }\\n }\\n\\n task \\"dev\\" {\\n driver = \\"raw_exec\\"\\n\\n env {\\n BOUNDARY_DEV_CONTROLLER_API_LISTEN_ADDRESS = local.private_ip\\n BOUNDARY_DEV_CONTROLLER_CLUSTER_LISTEN_ADDRESS = \\"0.0.0.0\\"\\n BOUNDARY_DEV_WORKER_PUBLIC_ADDRESS = local.public_ip\\n BOUNDARY_DEV_WORKER_PROXY_LISTEN_ADDRESS = local.private_ip\\n BOUNDARY_DEV_PASSWORD = \\"password\\"\\n }\\n\\n // artifact {\\n // source = \\"https://releases.hashicorp.com/boundary/${local.version}/boundary_${local.version}_linux_arm.zip\\"\\n // }\\n\\n config {\\n command = \\"boundary\\"\\n args = [\\"dev\\"]\\n }\\n\\n resources {\\n cpu = 500\\n memory = 500\\n }\\n\\n service {\\n name = \\"boundary\\"\\n tags = [\\"cluster\\"]\\n\\n port = \\"cluster\\"\\n\\n check {\\n type = \\"tcp\\"\\n interval = \\"10s\\"\\n timeout = \\"2s\\"\\n port = \\"api\\"\\n }\\n }\\n }\\n }\\n}\\n
UML 블록은 @startuml
과 @enduml
사이에 UML 구성을 위한 구성을 넣어 표기합니다. 아래와 같이 md 파일 내에 작성하면
@startuml
+Bob -> Alice : hello
+@enduml
+
다음과 같이 표기됩니다.
@startuml
Bob -> Alice : hello
@enduml
@startuml
+actor User
+interface Terraform
+cloud CLOUD
+
+User ->> Terraform : Apply
+User <<- Terraform : State
+Terraform ->> CLOUD : Probisioning
+CLOUD ->> Terraform : Response
+@enduml
+
@startuml
actor User
interface Terraform
cloud CLOUD
User ->> Terraform : Apply
User <<- Terraform : State
Terraform ->> CLOUD : Probisioning
CLOUD ->> Terraform : Response
@enduml
@startuml
+Alice -> Bob: Authentication Request
+Bob --> Alice: Authentication Response
+
+Alice -> Bob: Another authentication Request
+Alice <-- Bob: another authentication Response
+@enduml
+
@startuml
Alice -> Bob: Authentication Request
Bob --> Alice: Authentication Response
Alice -> Bob: Another authentication Request
Alice <-- Bob: another authentication Response
@enduml
@startuml
+:Main Admin: as Admin
+(Use the application) as (Use)
+
+User -> (Start)
+User --> (Use)
+
+Admin ---> (Use)
+
+note right of Admin : This is an example.
+
+note right of (Use)
+ A note can also
+ be on several lines
+end note
+
+note "This note is connected\\nto several objects." as N2
+(Start) .. N2
+N2 .. (Use)
+@enduml
+
@startuml
:Main Admin: as Admin
(Use the application) as (Use)
User -> (Start)
User --> (Use)
Admin ---> (Use)
note right of Admin : This is an example.
note right of (Use)
A note can also
be on several lines
end note
note "This note is connected\\nto several objects." as N2
(Start) .. N2
N2 .. (Use)
@enduml
@startuml
+Object <|-- Dummy
+
+class Dummy {
+ String data
+ void methods()
+ -field1
+ #field2
+ ~method1()
+ +method2()
+}
+
+class Flight {
+ flightNumber : Integer
+ departureTime : Date
+}
+
+class Car
+
+Driver - Car : drives >
+Car *- Wheel : have 4 >
+Car -- Person : < owns
+@enduml
+
@startuml
Object <|-- Dummy
class Dummy {
String data
void methods()
-field1
#field2
~method1()
+method2()
}
class Flight {
flightNumber : Integer
departureTime : Date
}
class Car
Driver - Car : drives >
Car *- Wheel : have 4 >
Car -- Person : < owns
@enduml
@startuml
+start
+partition Initialization {
+ :read config file;
+ :init internal variable;
+}
+partition Running {
+ if (multiprocessor?) then (yes)
+ fork
+ :Treatment 1;
+ fork again
+ :Treatment 2;
+ detach
+ end fork
+ else (monoproc)
+ :Treatment 1;
+ :Treatment 2;
+ endif
+}
+
+stop
+@enduml
+
@startuml
start
partition Initialization {
:read config file;
:init internal variable;
}
partition Running {
if (multiprocessor?) then (yes)
fork
:Treatment 1;
fork again
:Treatment 2;
detach
end fork
else (monoproc)
:Treatment 1;
:Treatment 2;
endif
}
stop
@enduml
@startuml
+package "Some Group" {
+ HTTP - [First Component]
+ [Another Component]
+}
+
+node "Other Groups" {
+ FTP - [Second Component]
+ [First Component] --> FTP
+}
+
+cloud {
+ [Example 1]
+}
+
+
+database "MySql" {
+ folder "This is my folder" {
+ [Folder 3]
+ }
+ frame "Foo" {
+ [Frame 4]
+ }
+}
+
+
+[Another Component] --> [Example 1]
+[Example 1] --> [Folder 3]
+[Folder 3] --> [Frame 4]
+@enduml
+
@startuml
package "Some Group" {
HTTP - [First Component]
[Another Component]
}
node "Other Groups" {
FTP - [Second Component]
[First Component] --> FTP
}
cloud {
[Example 1]
}
database "MySql" {
folder "This is my folder" {
[Folder 3]
}
frame "Foo" {
[Frame 4]
}
}
[Another Component] --> [Example 1]
[Example 1] --> [Folder 3]
[Folder 3] --> [Frame 4]
@enduml
@startuml
+[*] --> State1
+State1 --> [*]
+State1 : this is a string
+State1 : this is another string
+
+State1 -> State2
+State2 --> [*]
+
+scale 350 width
+[*] --> NotShooting
+
+state NotShooting {
+ [*] --> Idle
+ Idle --> Configuring : EvConfig
+ Configuring --> Idle : EvConfig
+}
+
+state Configuring {
+ [*] --> NewValueSelection
+ NewValueSelection --> NewValuePreview : EvNewValue
+ NewValuePreview --> NewValueSelection : EvNewValueRejected
+ NewValuePreview --> NewValueSelection : EvNewValueSaved
+
+ state NewValuePreview {
+ State1 -> State2
+ }
+}
+@enduml
+
@startuml
[] --> State1
State1 --> []
State1 : this is a string
State1 : this is another string
State1 -> State2
State2 --> [*]
scale 350 width
[*] --> NotShooting
state NotShooting {
[*] --> Idle
Idle --> Configuring : EvConfig
Configuring --> Idle : EvConfig
}
state Configuring {
[*] --> NewValueSelection
NewValueSelection --> NewValuePreview : EvNewValue
NewValuePreview --> NewValueSelection : EvNewValueRejected
NewValuePreview --> NewValueSelection : EvNewValueSaved
state NewValuePreview {
State1 -> State2
}
}
@enduml
@startuml
+nwdiag {
+ network dmz {
+ address = "210.x.x.x/24"
+
+ // set multiple addresses (using comma)
+ web01 [address = "210.x.x.1, 210.x.x.20"];
+ web02 [address = "210.x.x.2"];
+ }
+ network internal {
+ address = "172.x.x.x/24";
+
+ web01 [address = "172.x.x.1"];
+ web02 [address = "172.x.x.2"];
+ db01;
+ db02;
+ }
+}
+@enduml
+
@startuml
nwdiag {
network dmz {
address = "210.x.x.x/24"
// set multiple addresses (using comma)
+ web01 [address = "210.x.x.1, 210.x.x.20"];
+ web02 [address = "210.x.x.2"];
+
}
network internal {
address = "172.x.x.x/24";
web01 [address = "172.x.x.1"];
+ web02 [address = "172.x.x.2"];
+ db01;
+ db02;
+
}
}
@enduml
@startuml
+@startgantt
+[Prototype design] lasts 15 days
+[Test prototype] lasts 10 days
+-- All example --
+[Task 1 (1 day)] lasts 1 day
+[T2 (5 days)] lasts 5 days
+[T3 (1 week)] lasts 1 week
+[T4 (1 week and 4 days)] lasts 1 week and 4 days
+[T5 (2 weeks)] lasts 2 weeks
+@endgantt
+@enduml
+
@startuml
@startgantt
[Prototype design] lasts 15 days
[Test prototype] lasts 10 days
-- All example --
[Task 1 (1 day)] lasts 1 day
[T2 (5 days)] lasts 5 days
[T3 (1 week)] lasts 1 week
[T4 (1 week and 4 days)] lasts 1 week and 4 days
[T5 (2 weeks)] lasts 2 weeks
@endgantt
@enduml
@startuml
+@startmindmap
+* Debian
+** Ubuntu
+*** Linux Mint
+*** Kubuntu
+*** Lubuntu
+*** KDE Neon
+** LMDE
+** SolydXK
+** SteamOS
+** Raspbian with a very long name
+*** <s>Raspmbc</s> => OSMC
+*** <s>Raspyfi</s> => Volumio
+@endmindmap
+@enduml
+
+
@startuml
@startmindmap
markdown-it-plantuml 플러그인을 활성화 하여 UML 작성이 가능합니다. 아래는 플러그인 개발자의 안내를 풀어 일부 설명합니다.
\\nUML 블록은 @startuml
과 @enduml
사이에 UML 구성을 위한 구성을 넣어 표기합니다. 아래와 같이 md 파일 내에 작성하면
# Ubuntu
+# Install Go: https://github.com/golang/go/wiki/Ubuntu
+$ sudo add-apt-repository ppa:longsleep/golang-backports
+$ sudo apt update -y
+$ sudo apt install golang-go -y
+#sudo apt install golang-1.14-go -y
+
+# Build terraform-bundle from a release tag that matches your TF version
+# Otherwise you might get an error like:
+# "Failed to read config: this version of terraform-bundle can only build bundles for . . ."
+$ git clone https://github.com/hashicorp/terraform.git
+$ cd terraform
+$ go install ./tools/terraform-bundle
+
+#verify that terraform-bundle tool is there
+$ ls ~/go/bin/
+$ export PATH=\${PATH}:$HOME/go/bin/
+$ terraform-bundle --version
+0.13.0
+
bundle 구성할 명세를 hcl로 작성합니다. (e.g. tf-bundle.hcl)
팁
공식(Official) 프로바이더의 경우 source
정의를 생략할 수 있습니다. 그렇지 않는 경우에는 반드시 source
에 대한 정의가 필요합니다.
terraform {
+ # Version of Terraform to include in the bundle. An exact version number is required.
+ version = "0.15.4"
+}
+
+# Define which provider plugins are to be included
+providers {
+ null = {
+ versions = ["= 3.1.0"]
+ }
+ time = {
+ versions = ["= 0.7.1"]
+ }
+ random = {
+ versions = ["= 3.1.0"]
+ }
+ template = {
+ versions = ["= 2.2.0"]
+ }
+ tfe = {
+ versions = ["= 0.25.3"]
+ }
+ vsphere = {
+ versions = ["= 1.26.0"]
+ }
+ vault = {
+ versions = ["= 2.20.0"]
+ }
+ consul = {
+ versions = ["= 2.12.0"]
+ }
+ kubernetes = {
+ versions = ["= 2.2.0"]
+ }
+ ad = {
+ versions = ["=0.4.2"]
+ }
+ openstack = {
+ versions = ["= 1.42.0"]
+ source = "terraform-provider-openstack/openstack"
+ }
+ nsxt = {
+ versions = ["= 3.1.1"]
+ source = "vmware/nsxt"
+ }
+ vra7 = {
+ versions = ["= 3.0.2"]
+ source = "vmware/vra7"
+ }
+}
+
번들 생성은 다음과 같이 커맨드로 실행 합니다.
terraform-bundle package -os=linux -arch=amd64 tf-bundle.hcl
+
이 작업을 통해 Terraform Enterprise에서 기존 Terraform을 다운로드 받고 Provider를 다운로드 받던 동작을 미리 수행한 번들이 생성 됩니다.
생성된 번들 파일(zip)은 TFE Admin Console을 통해 적용
shasum -a256 ./terraform_0.15.4-bundle2021060202_linux_amd64.zip
\\n\\nhttps://github.com/hashicorp/terraform/tree/main/tools/terraform-bundle
\\n
\\nTerraform Enterprise에서 동작하는 기능입니다.
Airgap 환경에서 사용할 특정 버전의 Terraform과 여러 제공자 플러그인을 모두 포함하는 zip 파일 인 \\"번들 아카이브\\"를 생성하는 툴을 사용합니다. 일반적으로 Terraform init을 통해 특정 구성 작업에 필요한 플러그인을 다운로드하고 설치하지만 Airgap 환경에서는 공식 플러그인 저장소에 액세스 할 수 없는 경우가 발생합니다. Bundle 툴을 사용하여 Terraform 버전과 선택한 공급자를 모두 설치하기 위해 대상 시스템에 압축을 풀 수있는 zip 파일이 생성되므로 즉석 플러그인 설치가 필요하지 않습니다.
"}');export{x as comp,A as data}; diff --git a/assets/ProviderLocalFilesystem.html-XUIaiwWB.js b/assets/ProviderLocalFilesystem.html-XUIaiwWB.js new file mode 100644 index 0000000000..39e5493f93 --- /dev/null +++ b/assets/ProviderLocalFilesystem.html-XUIaiwWB.js @@ -0,0 +1,110 @@ +import{_ as r}from"./plugin-vue_export-helper-DlAUqK2U.js";import{r as o,o as i,c as l,b as n,d as a,a as e,e as t}from"./app-Bzk8Nrll.js";const p={},c=n("h1",{id:"terraform-provider-로컬-디렉토리",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#terraform-provider-로컬-디렉토리"},[n("span",null,"Terraform Provider - 로컬 디렉토리")])],-1),d={href:"https://www.terraform.io/docs/cli/config/config-file.html#implied-local-mirror-directories",target:"_blank",rel:"noopener noreferrer"},u=n("br",null,null,-1),m={href:"https://learn.hashicorp.com/tutorials/terraform/provider-use?in=terraform/providers",target:"_blank",rel:"noopener noreferrer"},v=t(`OS : CentOS7
NAME="CentOS Linux"
+VERSION="7 (Core)"
+ID="centos"
+ID_LIKE="rhel fedora"
+VERSION_ID="7"
+PRETTY_NAME="CentOS Linux 7 (Core)"
+ANSI_COLOR="0;31"
+CPE_NAME="cpe:/o:centos:centos:7"
+HOME_URL="https://www.centos.org/"
+BUG_REPORT_URL="https://bugs.centos.org/"
+
+CENTOS_MANTISBT_PROJECT="CentOS-7"
+CENTOS_MANTISBT_PROJECT_VERSION="7"
+REDHAT_SUPPORT_PRODUCT="centos"
+REDHAT_SUPPORT_PRODUCT_VERSION="7"
+
Terraform
$ terraform version
+Terraform v1.0.0
+
Plugin 디렉토리 구성
로컬 Provider를 찾기위한 디렉토리 구조를 생성합니다. host_name
은 환경마다 상이할 수 있습니다.
~/.terraform.d/plugins/\${host_name}/\${namespace}/\${type}/\${version}/\${target}
$ mkdir -p ~/.terraform.d/plugins/localhost.localdomain/vmware/nsxt/3.2.1/linux_amd64
+$ mkdir -p ~/.terraform.d/plugins/localhost.localdomain/hashicorp/random/3.1.0/linux_amd64
+
Provider 바이너리 파일 구성
기존에 받아놓은 zip 파일을 압축 해제하고, 생성한 Provider 디렉토리 각각에 맞는 프로바이더를 복사합니다.
$ unzip terraform-provider-random_3.1.0_linux_amd64.zip
+Archive: terraform-provider-random_3.1.0_linux_amd64.zip
+ inflating: terraform-provider-random_v3.1.0_x5
+
+$ mv ./terraform-provider-random_v3.1.0_x5 ~/.terraform.d/plugins/localhost.localdomain/hashicorp/random/3.1.0/linux_amd64
+
+$ unzip terraform-provider-nsxt_3.2.1_linux_amd64.zip
+Archive: terraform-provider-nsxt_3.2.1_linux_amd64.zip
+ inflating: CHANGELOG.md
+ inflating: LICENSE.txt
+ inflating: README.md
+ inflating: terraform-provider-nsxt_v3.2.1
+
+$ mv ./terraform-provider-nsxt_v3.2.1 ~/.terraform.d/plugins/localhost.localdomain/vmware/nsxt/3.2.1/linux_amd64
+
로컬 Provider 구성 확인
파일 구조
$ tree -a ~/.terraform.d/
+/root/.terraform.d/
+├── \`plugins\`
+│ └── localhost.localdomain
+│ ├── hashicorp
+│ │ └── random
+│ │ └── 3.1.0
+│ │ └── linux_amd64
+│ │ └── terraform-provider-random_v3.1.0_x5
+│ └── vmware
+│ └── nsxt
+│ └── 3.2.1
+│ └── linux_amd64
+│ └── terraform-provider-nsxt_v3.2.1
+├── checkpoint_cache
+└── checkpoint_signature
+
워크스페이스 생성 (디렉토리) - airgapped 는 임의의 이름 입니다.
$ mkdir ./airgapped
+$ cd ./airgapped
+
tf 파일 작성
$ cat <<EOF> terraform.tf
+terraform {
+ required_providers {
+ nsxt = {
+ source = "localhost.localdomain/vmware/nsxt"
+ version = "3.2.1"
+ }
+ random = {
+ source = "localhost.localdomain/hashicorp/random"
+ version = "3.1.0"
+ }
+ }
+}
+
+provider "nsxt" {
+ # Configuration options
+}
+
+provider "random" {
+ # Configuration options
+}
+
+resource "random_id" "test" {
+ byte_length = 8
+}
+
+output "random_id" {
+ value = random_id.test
+}
+EOF
+
Terraform init
을 수행하여 정상적으로 로컬 Provider를 가져오는지 확인합니다.
$ terraform init
+
+Initializing the backend...
+
+Initializing provider plugins...
+- Finding localhost.localdomain/vmware/nsxt versions matching "3.2.1"...
+- Finding localhost.localdomain/hashicorp/random versions matching "3.1.0"...
+- Installing localhost.localdomain/vmware/nsxt v3.2.1...
+- Installed localhost.localdomain/vmware/nsxt v3.2.1 (unauthenticated)
+- Installing localhost.localdomain/hashicorp/random v3.1.0...
+- Installed localhost.localdomain/hashicorp/random v3.1.0 (unauthenticated)
+
+Terraform has created a lock file .terraform.lock.hcl to record the provider
+selections it made above. Include this file in your version control repository
+so that Terraform can guarantee to make the same selections by default when
+you run "terraform init" in the future.
+
+Terraform has been successfully initialized!
+
+You may now begin working with Terraform. Try running "terraform plan" to see
+any changes that are required for your infrastructure. All Terraform commands
+should now work.
+
+If you ever set or change modules or backend configuration for Terraform,
+rerun this command to reinitialize your working directory. If you forget, other
+commands will detect it and remind you to do so if necessary.
+
\\n"}');export{E as comp,O as data}; diff --git a/assets/ProviderLocalMirroring.html-C7POxPIe.js b/assets/ProviderLocalMirroring.html-C7POxPIe.js new file mode 100644 index 0000000000..a01bf498c9 --- /dev/null +++ b/assets/ProviderLocalMirroring.html-C7POxPIe.js @@ -0,0 +1,15 @@ +import{_ as a}from"./plugin-vue_export-helper-DlAUqK2U.js";import{r as i,o as n,c as l,b as r,d as e,a as o}from"./app-Bzk8Nrll.js";const s={},c=r("h1",{id:"terraform-provider-로컬-미러링",tabindex:"-1"},[r("a",{class:"header-anchor",href:"#terraform-provider-로컬-미러링"},[r("span",null,"Terraform Provider - 로컬 미러링")])],-1),p={href:"https://www.terraform.io/docs/cli/config/config-file.html#provider_installation",target:"_blank",rel:"noopener noreferrer"},m={href:"http://registry.terraform.io/",target:"_blank",rel:"noopener noreferrer"},d=r("p",null,"하지만 네트워크이 느리거나 폐쇄망인 경우, 직접 다운로드가 아닌 다른 방법으로 프로바이더를 사용할 수 있습니다.",-1),h=r("p",null,"CLI 설정 파일에 명시적으로 설정하는 방법과 설정하지 않고 사용하는 방법이 있습니다.",-1),f=r("p",null,"상대적으로 설정이 간편한 filesystem_mirror 설정 방법은 다음과 같습니다.",-1),_=r("li",null,[r("p",null,"Terraform 사용 환경에 맞춰 terraform configuration 파일 구성하기"),r("ul",null,[r("li",null,"Windows : 사용자의 %APPDATA% 디렉토리 상에 terraform.rc"),r("li",null,"Linux/MacOS : 사용자 홈 디렉토리 상에 .terraformrc")])],-1),g=r("li",null,[r("p",null,"다음 처럼 'provider_installation' 설정하기"),r("div",{class:"language-text","data-ext":"text","data-title":"text"},[r("pre",{class:"language-text"},[r("code",null,`provider_installation { + filesystem_mirror { + path = "/usr/share/terraform/providers" + include = ["*/*"] # registry.terrafom.io/hashicorp/* + } +} +`)])])],-1),u=r("p",null,"대상 디렉토리 설정하기",-1),v=r("li",null,[r("p",null,"예를 들어 aws provider는 다음과 같이 코드 상에 사용"),r("div",{class:"language-text","data-ext":"text","data-title":"text"},[r("pre",{class:"language-text"},[r("code",null,`terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "3.36.0" + } + } +} +`)])])],-1),T=r("p",null,"지정된 경로 상에 다음과 같은 HOSTNAME/NAMESPACE/TYPE/VERSION/TARGET 형태로 디렉토리 구조를 지정",-1),y={href:"http://registry.terraform.io/",target:"_blank",rel:"noopener noreferrer"},x={href:"https://releases.hashicorp.com/",target:"_blank",rel:"noopener noreferrer"};function w(P,A){const t=i("ExternalLinkIcon");return n(),l("div",null,[c,r("blockquote",null,[r("p",null,[r("a",p,[e("https://www.terraform.io/docs/cli/config/config-file.html#provider_installation"),o(t)])])]),r("p",null,[e("Terraform CLI를 사용할 때, 기본적으로 코드 상에서 사용하는 플러그인은 "),r("a",m,[e("registry.terraform.io"),o(t)]),e("에서 다운로드 받게 되어 있습니다.")]),d,h,f,r("ol",null,[_,g,r("li",null,[u,r("ul",null,[v,r("li",null,[T,r("ul",null,[r("li",null,[e('HOSTNAME = "'),r("a",y,[e("registry.terraform.io"),o(t)]),e('", NAMESPACE="hashicorp", TYPE="aws", VERSION="3.36.0", TARGET은 클라이언트 환경에 대한 것으로 현재 실행 환경에 따라 "darwin_amd64", "linux_arm" "windows_amd64" 등으로 설정하시면 됩니다.')])])]),r("li",null,[r("p",null,[e("사용하시고자 하는 프로바이어더의 다운로드는 다음 링크에서 가능합니다. "),r("a",x,[e("https://releases.hashicorp.com"),o(t)])])])])])])])}const b=a(s,[["render",w],["__file","ProviderLocalMirroring.html.vue"]]),L=JSON.parse('{"path":"/04-HashiCorp/03-Terraform/05-Airgap/ProviderLocalMirroring.html","title":"Terraform Provider - 로컬 미러링","lang":"ko-KR","frontmatter":{"description":"공식 registry 접근이 불가능하거나 별도로 Provider를 관리해야 하는 경우 사용","tag":["terraform","provider"],"head":[["meta",{"property":"og:url","content":"https://docmoa.github.io/04-HashiCorp/03-Terraform/05-Airgap/ProviderLocalMirroring.html"}],["meta",{"property":"og:site_name","content":"docmoa"}],["meta",{"property":"og:title","content":"Terraform Provider - 로컬 미러링"}],["meta",{"property":"og:description","content":"공식 registry 접근이 불가능하거나 별도로 Provider를 관리해야 하는 경우 사용"}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"ko-KR"}],["meta",{"property":"og:updated_time","content":"2023-09-18T13:12:54.000Z"}],["meta",{"property":"article:tag","content":"terraform"}],["meta",{"property":"article:tag","content":"provider"}],["meta",{"property":"article:modified_time","content":"2023-09-18T13:12:54.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Terraform Provider - 로컬 미러링\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2023-09-18T13:12:54.000Z\\",\\"author\\":[]}"]]},"headers":[],"git":{"createdTime":1650413630000,"updatedTime":1695042774000,"contributors":[{"name":"Administrator","email":"admin@example.com","commits":1},{"name":"Great-Stone","email":"hahohh@gmail.com","commits":1}]},"readingTime":{"minutes":0.24,"words":72},"filePathRelative":"04-HashiCorp/03-Terraform/05-Airgap/ProviderLocalMirroring.md","localizedDate":"2022년 4월 20일","excerpt":"\\nhttps://www.terraform.io/docs/cli/config/config-file.html#implied-local-mirror-directories
\\n
\\nhttps://learn.hashicorp.com/tutorials/terraform/provider-use?in=terraform/providers
\\n\\nhttps://www.terraform.io/docs/cli/config/config-file.html#provider_installation
\\n
Terraform CLI를 사용할 때, 기본적으로 코드 상에서 사용하는 플러그인은 registry.terraform.io에서 다운로드 받게 되어 있습니다.
"}');export{b as comp,L as data}; diff --git a/assets/SSH Too many authentication failures.html-BcSkL8WN.js b/assets/SSH Too many authentication failures.html-BcSkL8WN.js new file mode 100644 index 0000000000..629b67599e --- /dev/null +++ b/assets/SSH Too many authentication failures.html-BcSkL8WN.js @@ -0,0 +1,14 @@ +import{_ as a}from"./plugin-vue_export-helper-DlAUqK2U.js";import{o as n,c as t,e as s,b as e}from"./app-Bzk8Nrll.js";const o={},i=s(`직역하자면 너무많은 인증 실패로 인한 SSH 접속이 안된다.
는 메시지를 간혹 보게되는 경우가 있다.
$ ssh myserver
+Received disconnect from 192.168.0.43 port 22:2: Too many authentication failures
+Connection to 192.168.0.43 closed by remote host.
+Connection to 192.168.0.43 closed.
+
특히나 클라우드나 VM을 새로 프로비저닝 해서 사용하려고 할때 IP가 중복되어 재사용되어야 하는 경우에 주로 발생하는 걸로 추측된다.
위 메시지의 발생 원인은 이미 SSH로 접속하려고 하는 클라이언트 환경에 많은 SSH ID 정보가 저장되어있고, SSH Client를 실행할 때 ssh-agent로 이미 알고있는 모든 SSH 키와 다른 모든 키에 대해 접속을 시도하게 된다. 이때 SSH로 접속하고자 하는 원격 서버는 특정 ID 키로 맵핑되어있고, 기존의 키 정보와 맞지 않거나 동일한 대상에 대한 SSH ID 정보와 달라진 것이 원인으로 확인된다.
접속하고자 하는 Client 환경에서 SSH 키를 초기화 하는 방법
$ ssh-add -D
+
위와 같이 했을 때 Could not open a connection to your authentication agent.
와 같은 오류가 발생한다면 다음 방법으로 초기화 한다.
$ exec ssh-agent bash
+$ ssh-add -D
+All identities removed.
+
SSH 옵션으로 Public Key를 이용한 접속을 일시적으로 사용하지 않도록 하는 방법
$ ssh -p 22 -o PubkeyAuthentication=no username@myserver
+
~/.ssh/config
의 대상 호스트에 IdentitiesOnly=yes
를 추가하는 벙법
많은 ID를 제공 하더라도 ssh가 ssh_config 파일에 구성된 인증 ID 파일만 사용하도록 지정한다고 함
직역하자면 너무많은 인증 실패로 인한 SSH 접속이 안된다.
는 메시지를 간혹 보게되는 경우가 있다.
$ ssh myserver\\nReceived disconnect from 192.168.0.43 port 22:2: Too many authentication failures\\nConnection to 192.168.0.43 closed by remote host.\\nConnection to 192.168.0.43 closed.\\n
팁
최대한 설정값을 넣어보고, 번역기도 돌려보고 물어도 보고 넣은 server설정 파일입니다.
네트워크는 프라이빗(온프레이머스) 환경입니다.
#nomad server 설정
+server {
+ enabled = true
+ bootstrap_expect = 3
+ license_path="/opt/nomad/license/nomad.license"
+ server_join {
+ retry_join = ["172.30.1.17","172.30.1.18","172.30.1.19"]
+ }
+ raft_protocol = 3
+ event_buffer_size = 100
+ non_voting_server = false
+ heartbeat_grace = "10s"
+}
+
+
+#tls 설정
+tls {
+ http = true
+ rpc = true
+
+ ca_file = "/opt/ssl/nomad/nomad-agent-ca.pem"
+ cert_file = "/opt/ssl/nomad/global-server-nomad-0.pem"
+ key_file = "/opt/ssl/nomad/global-server-nomad-0-key.pem"
+
+ #UI오픈할 서버만 변경
+ verify_server_hostname = false
+ verify_https_client = false
+ #일반서버는 아래와 같이 설정
+ verify_server_hostname = true
+ verify_https_client = true
+}
+
data_dir = "/opt/consul"
+
+client_addr = "0.0.0.0"
+
+datacenter = "my-dc"
+
+#ui
+ui_config {
+ enabled = true
+}
+
+# server
+server = true
+
+# Bind addr
+bind_addr = "0.0.0.0" # Listen on all IPv4
+# Advertise addr - if you want to point clients to a different address than bind or LB.
+advertise_addr = "node ip"
+
+# Enterprise License
+license_path = "/opt/nomad/nomad.lic"
+
+# bootstrap_expect
+bootstrap_expect=1
+
+# encrypt
+encrypt = "7w+zkhqa+YD4GSKXjRWETBIT8hs53Sr/w95oiVxq5Qc="
+
+# retry_join
+retry_join = ["server ip"]
+
+key_file = "/opt/consul/my-dc-server-consul-0-key.pem"
+cert_file = "/opt/consul/my-dc-server-consul-0.pem"
+ca_file = "/opt/consul/consul-agent-ca.pem"
+auto_encrypt {
+ allow_tls = true
+}
+
+verify_incoming = false
+verify_incoming_rpc = false
+verify_outgoing = false
+verify_server_hostname = false
+
+ports {
+ http = 8500
+ dns = 8600
+ server = 8300
+}
+
+
팁
\\n최대한 설정값을 넣어보고, 번역기도 돌려보고 물어도 보고 넣은 server설정 파일입니다.
\\n네트워크는 프라이빗(온프레이머스) 환경입니다.
#nomad server 설정\\nserver {\\n enabled = true\\n bootstrap_expect = 3\\n license_path=\\"/opt/nomad/license/nomad.license\\"\\n server_join {\\n retry_join = [\\"172.30.1.17\\",\\"172.30.1.18\\",\\"172.30.1.19\\"]\\n }\\n raft_protocol = 3\\n event_buffer_size = 100\\n non_voting_server = false\\n heartbeat_grace = \\"10s\\"\\n}\\n \\n \\n#tls 설정\\ntls {\\n http = true\\n rpc = true\\n \\n ca_file = \\"/opt/ssl/nomad/nomad-agent-ca.pem\\"\\n cert_file = \\"/opt/ssl/nomad/global-server-nomad-0.pem\\"\\n key_file = \\"/opt/ssl/nomad/global-server-nomad-0-key.pem\\"\\n \\n #UI오픈할 서버만 변경\\n verify_server_hostname = false\\n verify_https_client = false\\n #일반서버는 아래와 같이 설정\\n verify_server_hostname = true\\n verify_https_client = true\\n}\\n
현상
... googleapi: Error 400: Invalid request: Invalid request since instance is not running.
+
: Terraform을 통하지 않고 리소스가 삭제되어, 해당 리소스를 찾지 못하는 상황 발생
State 삭제
Local 환경의 terraform에 remote를 Terraform cloud로 지정
terraform {
+ required_version = ">= 0.12"
+ backend "remote" {
+ hostname = "app.terraform.io"
+ organization = "lguplus"
+
+ workspaces {
+ name = "kids_library"
+ }
+ }
+}
+
state 리스트 확인 terraform state list
my-workspace > terraform state list
+random_pet.sql
+module.Cluster_GKE.google_container_cluster.k8sexample
+module.Cluster_GKE.google_container_node_pool.pool_1
+module.Cluster_GKE.google_container_node_pool.pool_2
+module.gcs_buckets.google_storage_bucket.buckets[0]
+module.sql-db.google_sql_database.default
+module.sql-db.google_sql_database_instance.default
+module.sql-db.google_sql_user.default
+module.sql-db.null_resource.module_depends_on
+module.sql-db.random_id.user-password
+module.network.module.routes.google_compute_route.route["egress-internet"]
+module.network.module.subnets.google_compute_subnetwork.subnetwork["asia-northeast3/fc-kidslib-stg-subnet-1"]
+module.network.module.vpc.google_compute_network.network
+
존재하지 않는 resource를 삭제 terraform state rm [resource_name]
my-workspace > terraform state rm module.sql-db
+Removed module.sql-db.google_sql_database.default
+Removed module.sql-db.google_sql_database_instance.default
+Removed module.sql-db.google_sql_user.default
+Removed module.sql-db.null_resource.module_depends_on
+Removed module.sql-db.random_id.user-password
+Successfully removed 5 resource instance(s).
+
현상
\\n... googleapi: Error 400: Invalid request: Invalid request since instance is not running.\\n
: Terraform을 통하지 않고 리소스가 삭제되어, 해당 리소스를 찾지 못하는 상황 발생
\\nState 삭제
\\nLocal 환경의 terraform에 remote를 Terraform cloud로 지정
\\nterraform {\\n required_version = \\">= 0.12\\"\\n backend \\"remote\\" {\\n hostname = \\"app.terraform.io\\"\\n organization = \\"lguplus\\"\\n\\n workspaces {\\n name = \\"kids_library\\"\\n }\\n }\\n}\\n
state 리스트 확인 terraform state list
my-workspace > terraform state list\\nrandom_pet.sql\\nmodule.Cluster_GKE.google_container_cluster.k8sexample\\nmodule.Cluster_GKE.google_container_node_pool.pool_1\\nmodule.Cluster_GKE.google_container_node_pool.pool_2\\nmodule.gcs_buckets.google_storage_bucket.buckets[0]\\nmodule.sql-db.google_sql_database.default\\nmodule.sql-db.google_sql_database_instance.default\\nmodule.sql-db.google_sql_user.default\\nmodule.sql-db.null_resource.module_depends_on\\nmodule.sql-db.random_id.user-password\\nmodule.network.module.routes.google_compute_route.route[\\"egress-internet\\"]\\nmodule.network.module.subnets.google_compute_subnetwork.subnetwork[\\"asia-northeast3/fc-kidslib-stg-subnet-1\\"]\\nmodule.network.module.vpc.google_compute_network.network\\n
존재하지 않는 resource를 삭제 terraform state rm [resource_name]
my-workspace > terraform state rm module.sql-db\\nRemoved module.sql-db.google_sql_database.default\\nRemoved module.sql-db.google_sql_database_instance.default\\nRemoved module.sql-db.google_sql_user.default\\nRemoved module.sql-db.null_resource.module_depends_on\\nRemoved module.sql-db.random_id.user-password\\nSuccessfully removed 5 resource instance(s).\\n
Terraform Enterprise를 사용할 때, UI(https://TFE_SERVER) 상으로 접속할 수 없는 상황에서 비밀번호 변경이 필요한 경우, 아래와 같이 작업할 수 있다.
다음과 같이 수정 가능.
# 이전 버전의 TFE
+sudo docker exec -it ptfe_atlas /usr/bin/init.sh /app/scripts/wait-for-token -- bash -i -c 'cd /app && ./bin/rails c'
+## 수정 최신 버전의 TFE에서는 Container 이름이 변경됨 (2022.6.21)
+sudo docker exec -it tfe-atlas /usr/bin/init.sh /app/scripts/wait-for-token -- bash -i -c 'cd /app && ./bin/rails c'
+
irb(main):050:0> admin_user = User.find_by(username: "tfe-local-admin")
+=> #<User id: 33, email: "tfe-local-admin@test.com", username: "tfe-local-admin", is_admin: false, created_at: "2020-06-24 05:12:12", updated_at: "2020-07-01 09:12:25", suspended_at: nil, two_factor_delivery: nil, two_factor_sms_number: nil, two_factor_secret_key: nil, two_factor_recovery_index: 0, two_factor_recovery_secret_key: nil, two_factor_verified_at: nil, two_factor_enabled_at: nil, is_service_account: false, used_recovery_codes_encrypted: nil, last_auth_through_saml: nil, external_id: "user-361SGA3yMg3P1nGT", accepted_terms_at: nil, accepted_privacy_policy_at: nil, invitation_token: nil, invitation_created_at: nil, is_cyborg: false, onboarding_status: nil>
+irb(main):051:0> admin_user.password = '<<Password>>'
+=> "<<Password>>"
+irb(main):052:0> admin_user.password_confirmation = '<<Password>>'
+=> "<<Password>>"
+irb(main):053:0> admin_user.save
+2020-07-01 10:03:32 [DEBUG] {:msg=>"SettingStorage::Postgres failed to look up setting 'basic.base_domain'"}
+=> true
+
sudo docker exec -it ptfe_atlas /usr/bin/init.sh /app/scripts/wait-for-token -- bash -i -c 'cd /app && ./bin/rails c'
+user = User.find_by(email: "user@example.com")
+user.update(:password => '<<PASSWORD>>')
+user.save!
+
sudo docker exec -it ptfe_atlas /usr/bin/init.sh /app/scripts/wait-for-token -- bash -i -c 'cd /app && ./bin/rails c'
+user = User.find_by(email: "user@example.com")
+user.update(is_admin: true)
+user.save!
+
Terraform Enterprise를 사용할 때, UI(https://TFE_SERVER) 상으로 접속할 수 없는 상황에서 비밀번호 변경이 필요한 경우, 아래와 같이 작업할 수 있다.
\\n다음과 같이 수정 가능.
\\n# 이전 버전의 TFE\\nsudo docker exec -it ptfe_atlas /usr/bin/init.sh /app/scripts/wait-for-token -- bash -i -c 'cd /app && ./bin/rails c'\\n## 수정 최신 버전의 TFE에서는 Container 이름이 변경됨 (2022.6.21)\\nsudo docker exec -it tfe-atlas /usr/bin/init.sh /app/scripts/wait-for-token -- bash -i -c 'cd /app && ./bin/rails c'\\n
v202111-1 (582) 버전 이상으로 설치, 또는 업그레이드 시 발생하는 이슈
\\n\\nv202111-1 (582) 버전 이상으로 설치, 또는 업그레이드 시 발생하는 이슈
\\n
Nginx access Log | \\n
---|
2021/12/17 02:58:31 [error] 10#10: *913 connect(0mfailed (111: Connection refused) while connecting to upstream, client: 10.10.10.100, server:tfe.mydomain.com, reguest: \\"GET / HTTP/1.1\\", upstream: \\"http://172.11.0.1:9292/\\", host: \\"tfe.mydomain.com\\" | \\n
::: tabs
+@tab title
+__markdown content__
+
+@tab javascript
+\`\`\` javascript
+() => {
+ console.log('Javascript code example')
+}
+\`\`\`
+:::
+
다음과 같이 표기됩니다.
`,3),f=a("p",null,[a("strong",null,"markdown content")],-1),x=a("div",{class:"language-javascript","data-ext":"js","data-title":"js"},[a("pre",{class:"language-javascript"},[a("code",null,[a("span",{class:"token punctuation"},"("),a("span",{class:"token punctuation"},")"),n(),a("span",{class:"token operator"},"=>"),n(),a("span",{class:"token punctuation"},"{"),n(` + console`),a("span",{class:"token punctuation"},"."),a("span",{class:"token function"},"log"),a("span",{class:"token punctuation"},"("),a("span",{class:"token string"},"'Javascript code example'"),a("span",{class:"token punctuation"},")"),n(` +`),a("span",{class:"token punctuation"},"}"),n(` +`)])])],-1),w=c(`Code Tabs
는 코드 블록만을 표기하는 탭을 제공합니다.
::: code-tabs#shell
+
+@tab pnpm
+
+\`\`\`bash
+pnpm add -D vuepress-plugin-md-enhance
+\`\`\`
+
+@tab yarn
+
+\`\`\`bash
+yarn add -D vuepress-plugin-md-enhance
+\`\`\`
+
+@tab:active npm
+
+\`\`\`bash
+npm i -D vuepress-plugin-md-enhance
+\`\`\`
+
+:::
+
다음과 같이 표기됩니다.
`,4),y=a("div",{class:"language-bash","data-ext":"sh","data-title":"sh"},[a("pre",{class:"language-bash"},[a("code",null,[a("span",{class:"token function"},"pnpm"),n(),a("span",{class:"token function"},"add"),n(),a("span",{class:"token parameter variable"},"-D"),n(` vuepress-plugin-md-enhance +`)])])],-1),C=a("div",{class:"language-bash","data-ext":"sh","data-title":"sh"},[a("pre",{class:"language-bash"},[a("code",null,[a("span",{class:"token function"},"yarn"),n(),a("span",{class:"token function"},"add"),n(),a("span",{class:"token parameter variable"},"-D"),n(` vuepress-plugin-md-enhance +`)])])],-1),j=a("div",{class:"language-bash","data-ext":"sh","data-title":"sh"},[a("pre",{class:"language-bash"},[a("code",null,[a("span",{class:"token function"},"npm"),n(" i "),a("span",{class:"token parameter variable"},"-D"),n(` vuepress-plugin-md-enhance +`)])])],-1);function A(D,E){const l=o("ExternalLinkIcon"),p=o("Tabs"),d=o("CodeTabs");return u(),b("div",null,[v,a("p",null,[n("컨텐츠에 탭을 추가하여 상황에 따라 선택적으로 문서를 읽을 수 있도록 합니다."),h,n(" 상세 내용은 "),k,n("의 "),a("a",g,[n("Tabs"),i(l)]),n(", "),a("a",_,[n("Code Tabs"),i(l)]),n(" 를 확인해보세요.")]),T,i(p,{id:"13",data:[{id:"title"},{id:"javascript"}]},{title0:s(({value:e,isActive:t})=>[n("title")]),title1:s(({value:e,isActive:t})=>[n("javascript")]),tab0:s(({value:e,isActive:t})=>[f]),tab1:s(({value:e,isActive:t})=>[x]),_:1}),w,i(d,{id:"33",data:[{id:"pnpm"},{id:"yarn"},{id:"npm"}],active:2,"tab-id":"shell"},{title0:s(({value:e,isActive:t})=>[n("pnpm")]),title1:s(({value:e,isActive:t})=>[n("yarn")]),title2:s(({value:e,isActive:t})=>[n("npm")]),tab0:s(({value:e,isActive:t})=>[y]),tab1:s(({value:e,isActive:t})=>[C]),tab2:s(({value:e,isActive:t})=>[j]),_:1})])}const V=r(m,[["render",A],["__file","Tabs.html.vue"]]),B=JSON.parse('{"path":"/00-Howto/03-Tips/Tabs.html","title":"Tabs","lang":"ko-KR","frontmatter":{"description":"Tabs 컨텐츠에 탭을 추가하여 상황에 따라 선택적으로 문서를 읽을 수 있도록 합니다. 상세 내용은 Markdown Enhance의 Tabs, Code Tabs 를 확인해보세요. Tabs 기본 사용법 다음과 같이 표기됩니다. Code Tabs 기본 사용법 Code Tabs는 코드 블록만을 표기하는 탭을 제공합니다. ...","head":[["meta",{"property":"og:url","content":"https://docmoa.github.io/00-Howto/03-Tips/Tabs.html"}],["meta",{"property":"og:site_name","content":"docmoa"}],["meta",{"property":"og:title","content":"Tabs"}],["meta",{"property":"og:description","content":"Tabs 컨텐츠에 탭을 추가하여 상황에 따라 선택적으로 문서를 읽을 수 있도록 합니다. 상세 내용은 Markdown Enhance의 Tabs, Code Tabs 를 확인해보세요. Tabs 기본 사용법 다음과 같이 표기됩니다. Code Tabs 기본 사용법 Code Tabs는 코드 블록만을 표기하는 탭을 제공합니다. ..."}],["meta",{"property":"og:type","content":"article"}],["meta",{"property":"og:locale","content":"ko-KR"}],["meta",{"property":"og:updated_time","content":"2023-10-25T07:22:33.000Z"}],["meta",{"property":"article:modified_time","content":"2023-10-25T07:22:33.000Z"}],["script",{"type":"application/ld+json"},"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Article\\",\\"headline\\":\\"Tabs\\",\\"image\\":[\\"\\"],\\"dateModified\\":\\"2023-10-25T07:22:33.000Z\\",\\"author\\":[]}"]]},"headers":[{"level":2,"title":"Tabs 기본 사용법","slug":"tabs-기본-사용법","link":"#tabs-기본-사용법","children":[]},{"level":2,"title":"Code Tabs 기본 사용법","slug":"code-tabs-기본-사용법","link":"#code-tabs-기본-사용법","children":[]}],"git":{"createdTime":1634225909000,"updatedTime":1698218553000,"contributors":[{"name":"Great-Stone","email":"hahohh@gmail.com","commits":3},{"name":"Administrator","email":"admin@example.com","commits":1}]},"readingTime":{"minutes":0.4,"words":119},"filePathRelative":"00-Howto/03-Tips/Tabs.md","localizedDate":"2021년 10월 15일","excerpt":"\\n컨텐츠에 탭을 추가하여 상황에 따라 선택적으로 문서를 읽을 수 있도록 합니다.
\\n상세 내용은 Markdown Enhance
의 Tabs, Code Tabs 를 확인해보세요.
::: tip
+This is a tip
+:::
+
+::: warning
+This is a warning
+:::
+
+::: danger
+This is a dangerous warning
+:::
+
+::: details
+This is a details block, which does not work in IE / Edge
+:::
+
다음과 같이 표기됩니다.
팁
This is a tip
경고
This is a warning
위험
This is a dangerous warning
This is a details block, which does not work in IE / Edge
타입 우측에 타이틀명을 추가하여 기본 값을 변경합니다.
::: danger STOP
+Danger zone, do not proceed
+Go to [here](https://vuepress.vuejs.org/guide/markdown.html#:~:text=markdown.toc%20option.-,%23,-Custom%20Containers)
+:::
+
+::: details Click me to view the code
+\`\`\`js
+console.log('Hello, VuePress!')
+\`\`\`
+:::
+
console.log('Hello, VuePress!')
+
문서 작성시 팁과 주의사항을 표기하는 방법을 설명합니다.
\\n공식 문서
::: tip\\nThis is a tip\\n:::\\n\\n::: warning\\nThis is a warning\\n:::\\n\\n::: danger\\nThis is a dangerous warning\\n:::\\n\\n::: details\\nThis is a details block, which does not work in IE / Edge\\n:::\\n
job "nginx" {
+ datacenters = ["dc1"]
+
+ group "nginx" {
+
+ constraint {
+ attribute = "${attr.unique.hostname}"
+ value = "slave0"
+ }
+
+ #Vault tls가 있고 nomad client hcl 파일에 host volume이 명시되어 있는 설정 값
+ volume "cert-data" {
+ type = "host"
+ source = "cert-data"
+ read_only = false
+ }
+
+ #실패 없이 되라고 행운의 숫자인 7을 4번 줌
+ network {
+ port "http" {
+ to = 7777
+ static = 7777
+ }
+ }
+
+ service {
+ name = "nginx"
+ port = "http"
+ }
+
+ task "nginx" {
+ driver = "docker"
+
+ volume_mount {
+ volume = "cert-data"
+ destination = "/usr/local/cert"
+ }
+
+ config {
+ image = "nginx"
+
+ ports = ["http"]
+ volumes = [
+ "local:/etc/nginx/conf.d",
+
+ ]
+ }
+ template {
+ data = <<EOF
+#Vault는 active서버 1대외에는 전부 standby상태이며
+#서비스 호출 시(write)에는 active 서비스만 호출해야함으로 아래와 같이 consul에서 서비스를 불러옴
+
+upstream backend {
+{{ range service "active.vault" }}
+ server {{ .Address }}:{{ .Port }};
+{{ else }}server 127.0.0.1:65535; # force a 502
+{{ end }}
+}
+
+server {
+ listen 7777 ssl;
+ #위에서 nomad host volume을 mount한 cert를 가져옴
+ ssl on;
+ ssl_certificate /usr/local/cert/vault/global-client-vault-0.pem;
+ ssl_certificate_key /usr/local/cert/vault/global-client-vault-0-key.pem;
+ #vault ui 접근 시 / -> /ui redirect되기 때문에 location이 /외에는 되지 않는다.
+ location / {
+ proxy_pass https://backend;
+ }
+}
+EOF
+
+ destination = "local/load-balancer.conf"
+ change_mode = "signal"
+ change_signal = "SIGHUP"
+ }
+ resources {
+ cpu = 100
+ memory = 201
+ }
+ }
+ }
+}
+
job \\"nginx\\" {\\n datacenters = [\\"dc1\\"]\\n\\n group \\"nginx\\" {\\n\\n constraint {\\n attribute = \\"${attr.unique.hostname}\\"\\n value = \\"slave0\\"\\n }\\n\\n #Vault tls가 있고 nomad client hcl 파일에 host volume이 명시되어 있는 설정 값\\n volume \\"cert-data\\" {\\n type = \\"host\\"\\n source = \\"cert-data\\"\\n read_only = false\\n }\\n\\n #실패 없이 되라고 행운의 숫자인 7을 4번 줌\\n network {\\n port \\"http\\" {\\n to = 7777\\n static = 7777\\n }\\n }\\n\\n service {\\n name = \\"nginx\\"\\n port = \\"http\\"\\n }\\n\\n task \\"nginx\\" {\\n driver = \\"docker\\"\\n\\n volume_mount {\\n volume = \\"cert-data\\"\\n destination = \\"/usr/local/cert\\"\\n }\\n\\n config {\\n image = \\"nginx\\"\\n\\n ports = [\\"http\\"]\\n volumes = [\\n \\"local:/etc/nginx/conf.d\\",\\n\\n ]\\n }\\n template {\\n data = <<EOF \\n#Vault는 active서버 1대외에는 전부 standby상태이며 \\n#서비스 호출 시(write)에는 active 서비스만 호출해야함으로 아래와 같이 consul에서 서비스를 불러옴\\n\\nupstream backend {\\n{{ range service \\"active.vault\\" }}\\n server {{ .Address }}:{{ .Port }};\\n{{ else }}server 127.0.0.1:65535; # force a 502\\n{{ end }}\\n}\\n\\nserver {\\n listen 7777 ssl;\\n #위에서 nomad host volume을 mount한 cert를 가져옴\\n ssl on;\\n ssl_certificate /usr/local/cert/vault/global-client-vault-0.pem;\\n ssl_certificate_key /usr/local/cert/vault/global-client-vault-0-key.pem;\\n #vault ui 접근 시 / -> /ui redirect되기 때문에 location이 /외에는 되지 않는다.\\n location / {\\n proxy_pass https://backend;\\n }\\n}\\nEOF\\n\\n destination = \\"local/load-balancer.conf\\"\\n change_mode = \\"signal\\"\\n change_signal = \\"SIGHUP\\"\\n }\\n resources {\\n cpu = 100\\n memory = 201\\n }\\n }\\n }\\n}\\n
#수정 할 파일
+vi /etc/systemd/system/vmtoolsd.service
+
+[Unit]
+Description=Service for virtual machines hosted on VMware
+Documentation=http://open-vm-tools.sourceforge.net/about.php
+ConditionVirtualization=vmware
+DefaultDependencies=no
+Before=cloud-init-local.service
+#아래 After=dbus.service추가
+After=dbus.service
+After=vgauth.service
+After=apparmor.service
+RequiresMountsFor=/tmp
+After=systemd-remount-fs.service systemd-tmpfiles-setup.service systemd-modules-load.service
+
+[Service]
+ExecStart=/usr/bin/vmtoolsd
+TimeoutStopSec=5
+
+[Install]
+WantedBy=multi-user.target
+Alias=vmtoolsd.service
+
+
Consul ACL을 활성화 할 경우 default를 deny로 할 지 allow를 할 지 정할 수 있다.
deny로 할 경우에는 하나하나 policy로 tokne을 만들어서 사용해야 한다.
key_prefix "vault/" {
+ policy = "write"
+}
+service "vault" {
+ policy = "write"
+}
+agent_prefix "" {
+ policy = "read"
+}
+session_prefix "" {
+ policy = "write"
+}
+
node_prefix "" {
+ policy = "read"
+}
+service_prefix "" {
+ policy = "read"
+}
+# only needed if using prepared queries
+query_prefix "" {
+ policy = "read"
+}
+
service_prefix "" {
+ policy = "read"
+}
+key_prefix "" {
+ policy = "read"
+}
+node_prefix "" {
+ policy = "read"
+}
+
Consul ACL을 활성화 할 경우 default를 deny로 할 지 allow를 할 지 정할 수 있다.
\\ndeny로 할 경우에는 하나하나 policy로 tokne을 만들어서 사용해야 한다.
key_prefix \\"vault/\\" {\\n policy = \\"write\\"\\n}\\nservice \\"vault\\" {\\n policy = \\"write\\"\\n}\\nagent_prefix \\"\\" {\\n policy = \\"read\\"\\n}\\nsession_prefix \\"\\" {\\n policy = \\"write\\"\\n}\\n
\\n\\nFull name definition
\\n
\\nList of informationtechnology initialisms
z?De(k,P,D,!0,!1,Q):F(f,E,I,P,D,B,O,N,Q)},wn=(k,f,E,I,P,D,B,O,N)=>{let A=0;const z=f.length;let Q=k.length-1,X=z-1;for(;A<=Q&&A<=X;){const te=k[A],le=f[A]=N?Wn(f[A]):kn(f[A]);if(pt(te,le))w(te,le,E,null,P,D,B,O,N);else break;A++}for(;A<=Q&&A<=X;){const te=k[Q],le=f[X]=N?Wn(f[X]):kn(f[X]);if(pt(te,le))w(te,le,E,null,P,D,B,O,N);else break;Q--,X--}if(A>Q){if(A<=X){const te=X+1,le=teX)for(;A<=Q;)Ne(k[A],P,D,!0),A++;else{const te=A,le=A,ve=new Map;for(A=le;A<=X;A++){const nn=f[A]=N?Wn(f[A]):kn(f[A]);nn.key!=null&&ve.set(nn.key,A)}let Le,je=0;const mn=X-le+1;let wt=!1,Uo=0;const Xt=new Array(mn);for(A=0;A =mn){Ne(nn,P,D,!0);continue}let En;if(nn.key!=null)En=ve.get(nn.key);else for(Le=le;Le<=X;Le++)if(Xt[Le-le]===0&&pt(nn,f[Le])){En=Le;break}En===void 0?Ne(nn,P,D,!0):(Xt[En-le]=A+1,En>=Uo?Uo=En:wt=!0,w(nn,f[En],E,null,P,D,B,O,N),je++)}const zo=wt?Lh(Xt):St;for(Le=zo.length-1,A=mn-1;A>=0;A--){const nn=le+A,En=f[nn],Ko=nn+1 {const{el:D,type:B,transition:O,children:N,shapeFlag:A}=k;if(A&6){on(k.component.subTree,f,E,I);return}if(A&128){k.suspense.move(f,E,I);return}if(A&64){B.move(k,f,E,G);return}if(B===qe){a(D,f,E);for(let Q=0;Q O.enter(D),P);else{const{leave:Q,delayLeave:X,afterLeave:te}=O,le=()=>a(D,f,E),ve=()=>{Q(D,()=>{le(),te&&te()})};X?X(D,le,ve):ve()}else a(D,f,E)},Ne=(k,f,E,I=!1,P=!1)=>{const{type:D,props:B,ref:O,children:N,dynamicChildren:A,shapeFlag:z,patchFlag:Q,dirs:X}=k;if(O!=null&&us(O,null,E,k,!0),z&256){f.ctx.deactivate(k);return}const te=z&1&&X,le=!Vt(k);let ve;if(le&&(ve=B&&B.onVnodeBeforeUnmount)&&pn(ve,f,k),z&6)Cn(k.component,E,I);else{if(z&128){k.suspense.unmount(E,I);return}te&&xn(k,null,f,"beforeUnmount"),z&64?k.type.remove(k,f,E,P,G,I):A&&(D!==qe||Q>0&&Q&64)?De(A,f,E,!1,!0):(D===qe&&Q&384||!P&&z&16)&&De(N,f,E),I&&en(k)}(le&&(ve=B&&B.onVnodeUnmounted)||te)&&Ye(()=>{ve&&pn(ve,f,k),te&&xn(k,null,f,"unmounted")},E)},en=k=>{const{type:f,el:E,anchor:I,transition:P}=k;if(f===qe){Vn(E,I);return}if(f===ra){S(k);return}const D=()=>{s(E),P&&!P.persisted&&P.afterLeave&&P.afterLeave()};if(k.shapeFlag&1&&P&&!P.persisted){const{leave:B,delayLeave:O}=P,N=()=>B(E,D);O?O(k.el,D,N):N()}else D()},Vn=(k,f)=>{let E;for(;k!==f;)E=m(k),s(k),k=E;s(f)},Cn=(k,f,E)=>{const{bum:I,scope:P,update:D,subTree:B,um:O}=k;I&&as(I),P.stop(),D&&(D.active=!1,Ne(B,k,f,E)),O&&Ye(O,f),Ye(()=>{k.isUnmounted=!0},f),f&&f.pendingBranch&&!f.isUnmounted&&k.asyncDep&&!k.asyncResolved&&k.suspenseId===f.pendingId&&(f.deps--,f.deps===0&&f.resolve())},De=(k,f,E,I=!1,P=!1,D=0)=>{for(let B=D;B k.shapeFlag&6?L(k.component.subTree):k.shapeFlag&128?k.suspense.next():m(k.anchor||k.el);let U=!1;const $=(k,f,E)=>{k==null?f._vnode&&Ne(f._vnode,null,null,!0):w(f._vnode||null,k,f,null,null,null,E),U||(U=!0,er(),is(),U=!1),f._vnode=k},G={p:w,um:Ne,m:on,r:en,mt:Ie,mc:F,pc:W,pbc:j,n:L,o:e};let pe,we;return n&&([pe,we]=n(G)),{render:$,hydrate:pe,createApp:fh($,pe)}}function Js({type:e,props:n},t){return t==="svg"&&e==="foreignObject"||t==="mathml"&&e==="annotation-xml"&&n&&n.encoding&&n.encoding.includes("html")?void 0:t}function lt({effect:e,update:n},t){e.allowRecurse=n.allowRecurse=t}function gp(e,n){return(!e||e&&!e.pendingBranch)&&n&&!n.persisted}function kp(e,n,t=!1){const a=e.children,s=n.children;if(Z(a)&&Z(s))for(let l=0;l >1,e[t[r]] 0&&(n[a]=t[l-1]),t[l]=a)}}for(l=t.length,o=t[l-1];l-- >0;)t[l]=o,o=n[o];return t}function fp(e){const n=e.subTree.component;if(n)return n.asyncDep&&!n.asyncResolved?n:fp(n)}const Ph=e=>e.__isTeleport,qe=Symbol.for("v-fgt"),Nt=Symbol.for("v-txt"),sn=Symbol.for("v-cmt"),ra=Symbol.for("v-stc"),ia=[];let vn=null;function vp(e=!1){ia.push(vn=e?null:[])}function Ih(){ia.pop(),vn=ia[ia.length-1]||null}let fa=1;function dr(e){fa+=e}function _p(e){return e.dynamicChildren=fa>0?vn||St:null,Ih(),fa>0&&vn&&vn.push(e),e}function v_(e,n,t,a,s,l){return _p(wp(e,n,t,a,s,l,!0))}function bp(e,n,t,a,s){return _p(Ae(e,n,t,a,s,!0))}function ds(e){return e?e.__v_isVNode===!0:!1}function pt(e,n){return e.type===n.type&&e.key===n.key}const Ts="__vInternal",yp=({key:e})=>e??null,ss=({ref:e,ref_key:n,ref_for:t})=>(typeof e=="number"&&(e=""+e),e!=null?He(e)||$e(e)||ae(e)?{i:Fe,r:e,k:n,f:!!t}:e:null);function wp(e,n=null,t=null,a=0,s=null,l=e===qe?0:1,o=!1,r=!1){const c={__v_isVNode:!0,__v_skip:!0,type:e,props:n,key:n&&yp(n),ref:n&&ss(n),scopeId:Cs,slotScopeIds:null,children:t,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetAnchor:null,staticCount:0,shapeFlag:l,patchFlag:a,dynamicProps:s,dynamicChildren:null,appContext:null,ctx:Fe};return r?(ro(c,t),l&128&&e.normalize(c)):t&&(c.shapeFlag|=He(t)?8:16),fa>0&&!o&&vn&&(c.patchFlag>0||l&6)&&c.patchFlag!==32&&vn.push(c),c}const Ae=Ah;function Ah(e,n=null,t=null,a=0,s=null,l=!1){if((!e||e===Kd)&&(e=sn),ds(e)){const r=Zn(e,n,!0);return t&&ro(r,t),fa>0&&!l&&vn&&(r.shapeFlag&6?vn[vn.indexOf(e)]=r:vn.push(r)),r.patchFlag|=-2,r}if(Bh(e)&&(e=e.__vccOpts),n){n=Vh(n);let{class:r,style:c}=n;r&&!He(r)&&(n.class=Kl(r)),xe(c)&&(Ui(c)&&!Z(c)&&(c=Re({},c)),n.style=zl(c))}const o=He(e)?1:Jd(e)?128:Ph(e)?64:xe(e)?4:ae(e)?2:0;return wp(e,n,t,a,s,o,l,!0)}function Vh(e){return e?Ui(e)||Ts in e?Re({},e):e:null}function Zn(e,n,t=!1){const{props:a,ref:s,patchFlag:l,children:o}=e,r=n?Oh(a||{},n):a;return{__v_isVNode:!0,__v_skip:!0,type:e.type,props:r,key:r&&yp(r),ref:n&&n.ref?t&&s?Z(s)?s.concat(ss(n)):[s,ss(n)]:ss(n):s,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:o,target:e.target,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:n&&e.type!==qe?l===-1?16:l|16:l,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:e.transition,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&Zn(e.ssContent),ssFallback:e.ssFallback&&Zn(e.ssFallback),el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce}}function Cp(e=" ",n=0){return Ae(Nt,null,e,n)}function __(e,n){const t=Ae(ra,null,e);return t.staticCount=n,t}function b_(e="",n=!1){return n?(vp(),bp(sn,null,e)):Ae(sn,null,e)}function kn(e){return e==null||typeof e=="boolean"?Ae(sn):Z(e)?Ae(qe,null,e.slice()):typeof e=="object"?Wn(e):Ae(Nt,null,String(e))}function Wn(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:Zn(e)}function ro(e,n){let t=0;const{shapeFlag:a}=e;if(n==null)n=null;else if(Z(n))t=16;else if(typeof n=="object")if(a&65){const s=n.default;s&&(s._c&&(s._d=!1),ro(e,s()),s._c&&(s._d=!0));return}else{t=32;const s=n._;!s&&!(Ts in n)?n._ctx=Fe:s===3&&Fe&&(Fe.slots._===1?n._=1:(n._=2,e.patchFlag|=1024))}else ae(n)?(n={default:n,_ctx:Fe},t=32):(n=String(n),a&64?(t=16,n=[Cp(n)]):t=8);e.children=n,e.shapeFlag|=t}function Oh(...e){const n={};for(let t=0;t
Me||Fe;let hs,Cl;{const e=Si(),n=(t,a)=>{let s;return(s=e[t])||(s=e[t]=[]),s.push(a),l=>{s.length>1?s.forEach(o=>o(l)):s[0](l)}};hs=n("__VUE_INSTANCE_SETTERS__",t=>Me=t),Cl=n("__VUE_SSR_SETTERS__",t=>Pa=t)}const La=e=>{const n=Me;return hs(e),e.scope.on(),()=>{e.scope.off(),hs(n)}},hr=()=>{Me&&Me.scope.off(),hs(null)};function Ep(e){return e.vnode.shapeFlag&4}let Pa=!1;function Fh(e,n=!1){n&&Cl(n);const{props:t,children:a}=e.vnode,s=Ep(e);vh(e,t,s,n),yh(e,a);const l=s?Nh(e,n):void 0;return n&&Cl(!1),l}function Nh(e,n){const t=e.type;e.accessCache=Object.create(null),e.proxy=zi(new Proxy(e.ctx,ch));const{setup:a}=t;if(a){const s=e.setupContext=a.length>1?Mh(e):null,l=La(e);kt();const o=Qn(a,e,0,[e.props,s]);if(ft(),l(),Ei(o)){if(o.then(hr,hr),n)return o.then(r=>{mr(e,r,n)}).catch(r=>{Ta(r,e,0)});e.asyncDep=o}else mr(e,o,n)}else xp(e,n)}function mr(e,n,t){ae(n)?e.type.__ssrInlineRender?e.ssrRender=n:e.render=n:xe(n)&&(e.setupState=Ji(n)),xp(e,t)}let gr;function xp(e,n,t){const a=e.type;if(!e.render){if(!n&&gr&&!a.render){const s=a.template||lo(e).template;if(s){const{isCustomElement:l,compilerOptions:o}=e.appContext.config,{delimiters:r,compilerOptions:c}=a,p=Re(Re({isCustomElement:l,delimiters:r},o),c);a.render=gr(s,p)}}e.render=a.render||un}{const s=La(e);kt();try{uh(e)}finally{ft(),s()}}}function jh(e){return e.attrsProxy||(e.attrsProxy=new Proxy(e.attrs,{get(n,t){return Ze(e,"get","$attrs"),n[t]}}))}function Mh(e){const n=t=>{e.exposed=t||{}};return{get attrs(){return jh(e)},slots:e.slots,emit:e.emit,expose:n}}function Ss(e){if(e.exposed)return e.exposeProxy||(e.exposeProxy=new Proxy(Ji(zi(e.exposed)),{get(n,t){if(t in n)return n[t];if(t in la)return la[t](e)},has(n,t){return t in n||t in la}}))}function $h(e,n=!0){return ae(e)?e.displayName||e.name:e.name||n&&e.__name}function Bh(e){return ae(e)&&"__vccOpts"in e}const b=(e,n)=>Sd(e,n,Pa);function y_(e,n,t=Ee){const a=_t(),s=We(n),l=et(n),o=eo((c,p)=>{let d;return Yd(()=>{const h=e[n];Ln(d,h)&&(d=h,p())}),{get(){return c(),t.get?t.get(d):d},set(h){const m=a.vnode.props;!(m&&(n in m||s in m||l in m)&&(`onUpdate:${n}`in m||`onUpdate:${s}`in m||`onUpdate:${l}`in m))&&Ln(h,d)&&(d=h,p()),a.emit(`update:${n}`,t.set?t.set(h):h)}}}),r=n==="modelValue"?"modelModifiers":`${n}Modifiers`;return o[Symbol.iterator]=()=>{let c=0;return{next(){return c<2?{value:c++?e[r]||{}:o,done:!1}:{done:!0}}}},o}function i(e,n,t){const a=arguments.length;return a===2?xe(n)&&!Z(n)?ds(n)?Ae(e,null,[n]):Ae(e,n):Ae(e,null,n):(a>3?t=Array.prototype.slice.call(arguments,2):a===3&&ds(t)&&(t=[t]),Ae(e,n,t))}const Uh="3.4.19";/** +* @vue/runtime-dom v3.4.19 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/const zh="http://www.w3.org/2000/svg",Kh="http://www.w3.org/1998/Math/MathML",Gn=typeof document<"u"?document:null,kr=Gn&&Gn.createElement("template"),qh={insert:(e,n,t)=>{n.insertBefore(e,t||null)},remove:e=>{const n=e.parentNode;n&&n.removeChild(e)},createElement:(e,n,t,a)=>{const s=n==="svg"?Gn.createElementNS(zh,e):n==="mathml"?Gn.createElementNS(Kh,e):Gn.createElement(e,t?{is:t}:void 0);return e==="select"&&a&&a.multiple!=null&&s.setAttribute("multiple",a.multiple),s},createText:e=>Gn.createTextNode(e),createComment:e=>Gn.createComment(e),setText:(e,n)=>{e.nodeValue=n},setElementText:(e,n)=>{e.textContent=n},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>Gn.querySelector(e),setScopeId(e,n){e.setAttribute(n,"")},insertStaticContent(e,n,t,a,s,l){const o=t?t.previousSibling:n.lastChild;if(s&&(s===l||s.nextSibling))for(;n.insertBefore(s.cloneNode(!0),t),!(s===l||!(s=s.nextSibling)););else{kr.innerHTML=a==="svg"?``:a==="mathml"?``:e;const r=kr.content;if(a==="svg"||a==="mathml"){const c=r.firstChild;for(;c.firstChild;)r.appendChild(c.firstChild);r.removeChild(c)}n.insertBefore(r,t)}return[o?o.nextSibling:n.firstChild,t?t.previousSibling:n.lastChild]}},Bn="transition",Qt="animation",jt=Symbol("_vtc"),Fn=(e,{slots:n})=>i(Zd,Sp(e),n);Fn.displayName="Transition";const Tp={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},Jh=Fn.props=Re({},ap,Tp),ot=(e,n=[])=>{Z(e)?e.forEach(t=>t(...n)):e&&e(...n)},fr=e=>e?Z(e)?e.some(n=>n.length>1):e.length>1:!1;function Sp(e){const n={};for(const M in e)M in Tp||(n[M]=e[M]);if(e.css===!1)return n;const{name:t="v",type:a,duration:s,enterFromClass:l=`${t}-enter-from`,enterActiveClass:o=`${t}-enter-active`,enterToClass:r=`${t}-enter-to`,appearFromClass:c=l,appearActiveClass:p=o,appearToClass:d=r,leaveFromClass:h=`${t}-leave-from`,leaveActiveClass:m=`${t}-leave-active`,leaveToClass:g=`${t}-leave-to`}=e,v=Wh(s),w=v&&v[0],C=v&&v[1],{onBeforeEnter:_,onEnter:T,onEnterCancelled:y,onLeave:S,onLeaveCancelled:R,onBeforeAppear:x=_,onAppear:K=T,onAppearCancelled:F=y}=n,H=(M,ee,Ie)=>{zn(M,ee?d:r),zn(M,ee?p:o),Ie&&Ie()},j=(M,ee)=>{M._isLeaving=!1,zn(M,h),zn(M,g),zn(M,m),ee&&ee()},Y=M=>(ee,Ie)=>{const Se=M?K:T,J=()=>H(ee,M,Ie);ot(Se,[ee,J]),vr(()=>{zn(ee,M?c:l),Dn(ee,M?d:r),fr(Se)||_r(ee,a,w,J)})};return Re(n,{onBeforeEnter(M){ot(_,[M]),Dn(M,l),Dn(M,o)},onBeforeAppear(M){ot(x,[M]),Dn(M,c),Dn(M,p)},onEnter:Y(!1),onAppear:Y(!0),onLeave(M,ee){M._isLeaving=!0;const Ie=()=>j(M,ee);Dn(M,h),Pp(),Dn(M,m),vr(()=>{M._isLeaving&&(zn(M,h),Dn(M,g),fr(S)||_r(M,a,C,Ie))}),ot(S,[M,Ie])},onEnterCancelled(M){H(M,!1),ot(y,[M])},onAppearCancelled(M){H(M,!0),ot(F,[M])},onLeaveCancelled(M){j(M),ot(R,[M])}})}function Wh(e){if(e==null)return null;if(xe(e))return[Ws(e.enter),Ws(e.leave)];{const n=Ws(e);return[n,n]}}function Ws(e){return Xu(e)}function Dn(e,n){n.split(/\s+/).forEach(t=>t&&e.classList.add(t)),(e[jt]||(e[jt]=new Set)).add(n)}function zn(e,n){n.split(/\s+/).forEach(a=>a&&e.classList.remove(a));const t=e[jt];t&&(t.delete(n),t.size||(e[jt]=void 0))}function vr(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let Gh=0;function _r(e,n,t,a){const s=e._endId=++Gh,l=()=>{s===e._endId&&a()};if(t)return setTimeout(l,t);const{type:o,timeout:r,propCount:c}=Lp(e,n);if(!o)return a();const p=o+"end";let d=0;const h=()=>{e.removeEventListener(p,m),l()},m=g=>{g.target===e&&++d>=c&&h()};setTimeout(()=>{d (t[v]||"").split(", "),s=a(`${Bn}Delay`),l=a(`${Bn}Duration`),o=br(s,l),r=a(`${Qt}Delay`),c=a(`${Qt}Duration`),p=br(r,c);let d=null,h=0,m=0;n===Bn?o>0&&(d=Bn,h=o,m=l.length):n===Qt?p>0&&(d=Qt,h=p,m=c.length):(h=Math.max(o,p),d=h>0?o>p?Bn:Qt:null,m=d?d===Bn?l.length:c.length:0);const g=d===Bn&&/\b(transform|all)(,|$)/.test(a(`${Bn}Property`).toString());return{type:d,timeout:h,propCount:m,hasTransform:g}}function br(e,n){for(;e.length yr(t)+yr(e[a])))}function yr(e){return e==="auto"?0:Number(e.slice(0,-1).replace(",","."))*1e3}function Pp(){return document.body.offsetHeight}function Yh(e,n,t){const a=e[jt];a&&(n=(n?[n,...a]:[...a]).join(" ")),n==null?e.removeAttribute("class"):t?e.setAttribute("class",n):e.className=n}const va=Symbol("_vod"),w_={beforeMount(e,{value:n},{transition:t}){e[va]=e.style.display==="none"?"":e.style.display,t&&n?t.beforeEnter(e):Zt(e,n)},mounted(e,{value:n},{transition:t}){t&&n&&t.enter(e)},updated(e,{value:n,oldValue:t},{transition:a}){!n==!t&&(e.style.display===e[va]||!n)||(a?n?(a.beforeEnter(e),Zt(e,!0),a.enter(e)):a.leave(e,()=>{Zt(e,!1)}):Zt(e,n))},beforeUnmount(e,{value:n}){Zt(e,n)}};function Zt(e,n){e.style.display=n?e[va]:"none"}const Xh=Symbol(""),Qh=/(^|;)\s*display\s*:/;function Zh(e,n,t){const a=e.style,s=He(t),l=a.display;let o=!1;if(t&&!s){if(n&&!He(n))for(const r in n)t[r]==null&&El(a,r,"");for(const r in t)r==="display"&&(o=!0),El(a,r,t[r])}else if(s){if(n!==t){const r=a[Xh];r&&(t+=";"+r),a.cssText=t,o=Qh.test(t)}}else n&&e.removeAttribute("style");va in e&&(e[va]=o?a.display:"",a.display=l)}const wr=/\s*!important$/;function El(e,n,t){if(Z(t))t.forEach(a=>El(e,n,a));else if(t==null&&(t=""),n.startsWith("--"))e.setProperty(n,t);else{const a=em(e,n);wr.test(t)?e.setProperty(et(a),t.replace(wr,""),"important"):e[a]=t}}const Cr=["Webkit","Moz","ms"],Gs={};function em(e,n){const t=Gs[n];if(t)return t;let a=We(n);if(a!=="filter"&&a in e)return Gs[n]=a;a=Kt(a);for(let s=0;s Ys||(om.then(()=>Ys=0),Ys=Date.now());function im(e,n){const t=a=>{if(!a._vts)a._vts=Date.now();else if(a._vts<=t.attached)return;dn(pm(a,t.value),n,5,[a])};return t.value=e,t.attached=rm(),t}function pm(e,n){if(Z(n)){const t=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{t.call(e),e._stopped=!0},n.map(a=>s=>!s._stopped&&a&&a(s))}else return n}const Sr=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,cm=(e,n,t,a,s,l,o,r,c)=>{const p=s==="svg";n==="class"?Yh(e,a,p):n==="style"?Zh(e,t,a):Ea(n)?$l(n)||sm(e,n,t,a,o):(n[0]==="."?(n=n.slice(1),!0):n[0]==="^"?(n=n.slice(1),!1):um(e,n,a,p))?tm(e,n,a,l,o,r,c):(n==="true-value"?e._trueValue=a:n==="false-value"&&(e._falseValue=a),nm(e,n,a,p))};function um(e,n,t,a){if(a)return!!(n==="innerHTML"||n==="textContent"||n in e&&Sr(n)&&ae(t));if(n==="spellcheck"||n==="draggable"||n==="translate"||n==="form"||n==="list"&&e.tagName==="INPUT"||n==="type"&&e.tagName==="TEXTAREA")return!1;if(n==="width"||n==="height"){const s=e.tagName;if(s==="IMG"||s==="VIDEO"||s==="CANVAS"||s==="SOURCE")return!1}return Sr(n)&&He(t)?!1:n in e}const Ip=new WeakMap,Ap=new WeakMap,ms=Symbol("_moveCb"),Lr=Symbol("_enterCb"),Vp={name:"TransitionGroup",props:Re({},Jh,{tag:String,moveClass:String}),setup(e,{slots:n}){const t=_t(),a=tp();let s,l;return op(()=>{if(!s.length)return;const o=e.moveClass||`${e.name||"v"}-move`;if(!fm(s[0].el,t.vnode.el,o))return;s.forEach(mm),s.forEach(gm);const r=s.filter(km);Pp(),r.forEach(c=>{const p=c.el,d=p.style;Dn(p,o),d.transform=d.webkitTransform=d.transitionDuration="";const h=p[ms]=m=>{m&&m.target!==p||(!m||/transform$/.test(m.propertyName))&&(p.removeEventListener("transitionend",h),p[ms]=null,zn(p,o))};p.addEventListener("transitionend",h)})}),()=>{const o=ie(e),r=Sp(o);let c=o.tag||qe;s=l,l=n.default?ao(n.default()):[];for(let p=0;p delete e.mode;Vp.props;const hm=Vp;function mm(e){const n=e.el;n[ms]&&n[ms](),n[Lr]&&n[Lr]()}function gm(e){Ap.set(e,e.el.getBoundingClientRect())}function km(e){const n=Ip.get(e),t=Ap.get(e),a=n.left-t.left,s=n.top-t.top;if(a||s){const l=e.el.style;return l.transform=l.webkitTransform=`translate(${a}px,${s}px)`,l.transitionDuration="0s",e}}function fm(e,n,t){const a=e.cloneNode(),s=e[jt];s&&s.forEach(r=>{r.split(/\s+/).forEach(c=>c&&a.classList.remove(c))}),t.split(/\s+/).forEach(r=>r&&a.classList.add(r)),a.style.display="none";const l=n.nodeType===1?n:n.parentNode;l.appendChild(a);const{hasTransform:o}=Lp(a);return l.removeChild(a),o}const Pr=e=>{const n=e.props["onUpdate:modelValue"]||!1;return Z(n)?t=>as(n,t):n};function vm(e){e.target.composing=!0}function Ir(e){const n=e.target;n.composing&&(n.composing=!1,n.dispatchEvent(new Event("input")))}const Xs=Symbol("_assign"),C_={created(e,{modifiers:{lazy:n,trim:t,number:a}},s){e[Xs]=Pr(s);const l=a||s.props&&s.props.type==="number";Et(e,n?"change":"input",o=>{if(o.target.composing)return;let r=e.value;t&&(r=r.trim()),l&&(r=ml(r)),e[Xs](r)}),t&&Et(e,"change",()=>{e.value=e.value.trim()}),n||(Et(e,"compositionstart",vm),Et(e,"compositionend",Ir),Et(e,"change",Ir))},mounted(e,{value:n}){e.value=n??""},beforeUpdate(e,{value:n,modifiers:{lazy:t,trim:a,number:s}},l){if(e[Xs]=Pr(l),e.composing)return;const o=s||e.type==="number"?ml(e.value):e.value,r=n??"";o!==r&&(document.activeElement===e&&e.type!=="range"&&(t||a&&e.value.trim()===r)||(e.value=r))}},_m=["ctrl","shift","alt","meta"],bm={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,n)=>_m.some(t=>e[`${t}Key`]&&!n.includes(t))},E_=(e,n)=>{const t=e._withMods||(e._withMods={}),a=n.join(".");return t[a]||(t[a]=(s,...l)=>{for(let o=0;o {const t=e._withKeys||(e._withKeys={}),a=n.join(".");return t[a]||(t[a]=s=>{if(!("key"in s))return;const l=et(s.key);if(n.some(o=>o===l||ym[o]===l))return e(s)})},wm=Re({patchProp:cm},qh);let Qs,Ar=!1;function Cm(){return Qs=Ar?Qs:Th(wm),Ar=!0,Qs}const Em=(...e)=>{const n=Cm().createApp(...e),{mount:t}=n;return n.mount=a=>{const s=Tm(a);if(s)return t(s,!0,xm(s))},n};function xm(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function Tm(e){return He(e)?document.querySelector(e):e}var Sm=["link","meta","script","style","noscript","template"],Lm=["title","base"],Pm=([e,n,t])=>Lm.includes(e)?e:Sm.includes(e)?e==="meta"&&n.name?`${e}.${n.name}`:e==="template"&&n.id?`${e}.${n.id}`:JSON.stringify([e,Object.entries(n).map(([a,s])=>typeof s=="boolean"?s?[a,""]:null:[a,s]).filter(a=>a!=null).sort(([a],[s])=>a.localeCompare(s)),t]):null,Im=e=>{const n=new Set,t=[];return e.forEach(a=>{const s=Pm(a);s&&!n.has(s)&&(n.add(s),t.push(a))}),t},Am=e=>e[0]==="/"?e:`/${e}`,io=e=>e[e.length-1]==="/"||e.endsWith(".html")?e:`${e}/`,yn=e=>/^(https?:)?\/\//.test(e),Vm=/.md((\?|#).*)?$/,_a=(e,n="/")=>!!(yn(e)||e.startsWith("/")&&!e.startsWith(n)&&!Vm.test(e)),Op=e=>/^[a-z][a-z0-9+.-]*:/.test(e),_n=e=>Object.prototype.toString.call(e)==="[object Object]",Om=e=>{const[n,...t]=e.split(/(\?|#)/);if(!n||n.endsWith("/"))return e;let a=n.replace(/(^|\/)README.md$/i,"$1index.html");return a.endsWith(".md")?a=a.substring(0,a.length-3)+".html":a.endsWith(".html")||(a=a+".html"),a.endsWith("/index.html")&&(a=a.substring(0,a.length-10)),a+t.join("")},Ls=e=>e[e.length-1]==="/"?e.slice(0,-1):e,Dp=e=>e[0]==="/"?e.slice(1):e,Dm=(e,n)=>{const t=Object.keys(e).sort((a,s)=>{const l=s.split("/").length-a.split("/").length;return l!==0?l:s.length-a.length});for(const a of t)if(n.startsWith(a))return a;return"/"},Rp=e=>typeof e=="function",me=e=>typeof e=="string";const Rm="modulepreload",Hm=function(e){return"/"+e},Vr={},u=function(n,t,a){let s=Promise.resolve();if(t&&t.length>0){const l=document.getElementsByTagName("link");s=Promise.all(t.map(o=>{if(o=Hm(o),o in Vr)return;Vr[o]=!0;const r=o.endsWith(".css"),c=r?'[rel="stylesheet"]':"";if(!!a)for(let h=l.length-1;h>=0;h--){const m=l[h];if(m.href===o&&(!r||m.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${o}"]${c}`))return;const d=document.createElement("link");if(d.rel=r?"stylesheet":Rm,r||(d.as="script",d.crossOrigin=""),d.href=o,document.head.appendChild(d),r)return new Promise((h,m)=>{d.addEventListener("load",h),d.addEventListener("error",()=>m(new Error(`Unable to preload CSS for ${o}`)))})}))}return s.then(()=>n()).catch(l=>{const o=new Event("vite:preloadError",{cancelable:!0});if(o.payload=l,window.dispatchEvent(o),!o.defaultPrevented)throw l})},Fm=JSON.parse("{}"),Nm=Object.fromEntries([["/",{loader:()=>u(()=>import("./index.html-D4cWqosL.js"),__vite__mapDeps([0,1])),meta:{d:1627556976e3,e:``,r:{minutes:1.14,words:341},y:"h",t:"docmoa",i:"home"}}],["/00-Howto/01-Overview.html",{loader:()=>u(()=>import("./01-Overview.html-BtGYf7na.js"),__vite__mapDeps([2,3,1])),meta:{d:1634225909e3,e:` + 문서가 인터넷상에 공개되는 목적은 접근성을 극대화 하기 위함 입니다. 또한 로컬환경에서 빠르게 문서를 검색하기 위해 해당 git repo를 clone 받거나 download 받아서 별도의 마크다운 툴과 연동하는 것도 가능합니다.
+VuePress
기반으로 구성되었기 때문에 이외의 방식은 문서 표기에 제약이 있을 수 있습니다.github page
+docmoa의 공개된 페이지를 통해 문서를 읽을 수 있습니다.
`,r:{minutes:.64,words:192},y:"a",t:"docmoa 활용 가이드"}}],["/00-Howto/",{loader:()=>u(()=>import("./index.html-D-7U4fdm.js"),__vite__mapDeps([4,1])),meta:{d:1628085698e3,e:` +문서를 올바르게 작성하고 공유하기 위한 몇가지 사항을 안내합니다.
+++안내
+문서 기여 시 문서 작성 가이드를 꼭 한번 확인해주세요.
+활용 가이드
+docmoa를 활용할 수 있는 몇가지 가이드를 docmoa 활용 가이드 에서 설명합니다.
`,r:{minutes:.26,words:77},y:"a",t:'How to "docmoa"'}}],["/01-Infrastructure/",{loader:()=>u(()=>import("./index.html-Da7Zh9h7.js"),__vite__mapDeps([5,1])),meta:{d:1628085698e3,g:["Infrastructure"],e:` +Recent pages
++
`,r:{minutes:.19,words:58},y:"a",t:"Infrastructure"}}],["/02-PrivatePlatform/",{loader:()=>u(()=>import("./index.html-BikDGuPB.js"),__vite__mapDeps([6,1])),meta:{d:1695042774e3,g:["Platform"],e:` +- + {{ page.title }} + + [ {{ (new Date(page.frontmatter.date)).toLocaleString() }} ] + +
+Recent pages
++
`,r:{minutes:.2,words:59},y:"a",t:"Private Platform"}}],["/03-PublicCloud/",{loader:()=>u(()=>import("./index.html-Br7ta8RV.js"),__vite__mapDeps([7,1])),meta:{d:1695042774e3,g:["Cloud"],e:` +- + {{ page.title }} + + [ {{ (new Date(page.frontmatter.date)).toLocaleString() }} ] + +
+Recent pages
++
+`,r:{minutes:.2,words:59},y:"a",t:"Public Cloud"}}],["/04-HashiCorp/",{loader:()=>u(()=>import("./index.html-CJ-C5MJx.js"),__vite__mapDeps([8,1])),meta:{d:1628085698e3,g:["HashiCorp"],e:` +- + {{ page.title }} + + [ {{ (new Date(page.frontmatter.date)).toLocaleString() }} ] + +
+Recent pages
++
+- + {{ page.title }} + + [ {{ (new Date(page.frontmatter.date)).toLocaleString() }} ] + +
+Packer
++`,r:{minutes:1.31,words:393},y:"a",t:"HashiCorp"}}],["/05-Software/",{loader:()=>u(()=>import("./index.html-BcBPf8Rf.js"),__vite__mapDeps([9,1])),meta:{d:164032788e4,g:["Software"],e:` +다양한 플랫폼에 대한 VM, 컨테이너 이미지 생성 자동화 도구
+Recent pages
++
+- + {{ page.title }} + + [ {{ (new Date(page.frontmatter.date)).toLocaleString() }} ] + +
+Jenkins
+`,r:{minutes:.21,words:62},y:"a",t:"Software"}}],["/06-etc/",{loader:()=>u(()=>import("./index.html-C5t7F1jk.js"),__vite__mapDeps([10,1])),meta:{d:1640259507e3,g:["Etc"],e:` +Recent pages
++
+`,r:{minutes:.19,words:58},y:"a",t:"Etc."}}],["/99-about/01-About.html",{loader:()=>u(()=>import("./01-About.html-7Tx-G8Iy.js"),__vite__mapDeps([11,1])),meta:{d:1628085698e3,e:` + +- + {{ page.title }} + + [ {{ (new Date(page.frontmatter.date)).toLocaleString() }} ] + +
+기술은 지속적으로 발전하고 시시각각 변화하고 있습니다. 더불어 IT라는 분야도 점점더 세분화되고, 혼자서는 모든것을 아는것은 거의 불가능합니다.
+
+IT 업을 하면서 정리와 스크랩은 일상이 되어가지만 변화를 쫓아가기는 정말 버겁습니다.하지만 혼자서가 아니라면 어떨까 라는 생각을 합니다.
`,r:{minutes:.26,words:78},y:"a",t:"docmoa"}}],["/99-about/02-Thanks.html",{loader:()=>u(()=>import("./02-Thanks.html-BWdU5ZFB.js"),__vite__mapDeps([12,1])),meta:{d:1628085698e3,e:` +`,r:{minutes:.3,words:89},y:"a",t:"Thank you"}}],["/00-Howto/02-Guide/01-Start.html",{loader:()=>u(()=>import("./01-Start.html-CZXt5onJ.js"),__vite__mapDeps([13,3,1])),meta:{d:1634225909e3,e:` +
+집단지성 이란 표현이 있듯, 개인보다는 여럿이 만들어가는 노트입니다. 과거에는 이런 노하우가 개인의 자산으로 비밀처럼 감춰두던 지식이였지만 지금은 서로 공유하고, 알리고, 기여하는 것도 의미 있는 시기인것 같습니다.docmoa에 문서 기여하기위한 가이드를 설명합니다.
+++팁
+다양한 방법으로 문서를 작성하고 기여할 수 있습니다.
+
+얽매이지 마세요.문서는 모두 git으로 관리되며 공개되어있습니다. 문서 기여를 위한 방식은 별도 안내로 구분하여 설명합니다.
`,r:{minutes:.81,words:242},y:"a",t:"문서작성 '시작'"}}],["/00-Howto/02-Guide/02-Contribute.html",{loader:()=>u(()=>import("./02-Contribute.html-CujV57Pk.js"),__vite__mapDeps([14,1])),meta:{d:1634225909e3,e:` +docmoa에 문서 기여하기위한 가이드를 설명합니다. 일반적인 github 상에서의 코드 기여 방식과 동일합니다.
+git 설치(Option)
+로컬 환경에서 git 명령을 수행하기 위해 설치합니다. github 브라우저 환경에서 수정하는 것도 가능하지만, 로컬에서 문서를 활용하고 오프라인 작업을 위해서는 설치하시기를 권장합니다.
+Git 설치 방법 안내를 참고하여 아래 설명합니다.
`,r:{minutes:1.12,words:335},y:"a",t:"Contribute"}}],["/00-Howto/02-Guide/04-Template.html",{loader:()=>u(()=>import("./04-Template.html-D_srWqRi.js"),__vite__mapDeps([15,1])),meta:{d:1634225909e3,e:` +docmoa에 문서 템플릿을 설명합니다.
+++주의
+기본 템플릿 가이드를 잘 지켜주세요. 함께 만드는 문서 모음이므로, 기본적인 형식이 필요합니다.
+최소 Template
+`,r:{minutes:.15,words:44},y:"a",t:"Template"}}],["/00-Howto/02-Guide/",{loader:()=>u(()=>import("./index.html-D-y1LV_e.js"),__vite__mapDeps([16,1])),meta:{d:1695042774e3,e:` +`,r:{minutes:.02,words:5},y:"a",t:"Guides"}}],["/00-Howto/03-Tips/Chart.html",{loader:()=>u(()=>import("./Chart.html-Dir6u0uC.js"),__vite__mapDeps([17,1])),meta:{d:1663987184e3,e:` +--- + +--- + +# h1 제목 = Title 입니다. +내용은 마크다운 형식으로 작성합니다. + +## h2 제목 + +
문서 작성시 차트를 추가하는 방법을 안내합니다.
++
+- 공식 문서
+차트 구성 방식은 ChartJS를 따릅니다.
+`,r:{minutes:.92,words:277},y:"a",t:"Chart"}}],["/00-Howto/03-Tips/CodeBlock.html",{loader:()=>u(()=>import("./CodeBlock.html-2_d1c3y7.js"),__vite__mapDeps([18,1])),meta:{d:1634225909e3,e:'\n
::: chart
와:::
로 처리합니다.마크다운 기본 사용 법과 거의 동일합니다.
\n기본 사용법
\n코드블록은 ``` 과 ``` 사이에 코드를 넣어 로 표기합니다. 아래와 같이 md 파일 내에 작성하면
\n',r:{minutes:.66,words:197},y:"a",t:"Code Block"}}],["/00-Howto/03-Tips/Link.html",{loader:()=>u(()=>import("./Link.html-Bu_HIehI.js"),__vite__mapDeps([19,1])),meta:{d:1634225909e3,e:` +```\n# Code block e.g.\nThis is my code\n```\n
문서 작성시 외부 링크를 포함하는 예를 설명합니다. 참고 문서
+텍스트에 링크 달기
+설명하던 글의 특정 단어에 대해 외부 링크를 추가하고자 하는 경우 브라킷
+[ ]
과 괄호를 사용합니다. domain을 같이 기입하는 경우 새창에서 열기로 표기됩니다.`,r:{minutes:.44,words:133},y:"a",t:"Link"}}],["/00-Howto/03-Tips/PlantUML.html",{loader:()=>u(()=>import("./PlantUML.html-C1o0QjFB.js"),__vite__mapDeps([20,1])),meta:{d:1634225909e3,e:` +새창으로 이동하는 [링크 달기](http://docmoa.github.io/00-Howto/03-Tips/Link.html) +현재 창에서 이동하는 [링크 달기](/00-Howto/03-Tips/Link.html) +
markdown-it-plantuml 플러그인을 활성화 하여 UML 작성이 가능합니다. 아래는 플러그인 개발자의 안내를 풀어 일부 설명합니다.
+기본 사용법
+UML 블록은
`,r:{minutes:2.77,words:831},y:"a",t:"UML"}}],["/00-Howto/03-Tips/",{loader:()=>u(()=>import("./index.html-Dr0IN6A8.js"),__vite__mapDeps([21,1])),meta:{d:1695042774e3,e:` +`,r:{minutes:.02,words:5},y:"a",t:"Tips"}}],["/00-Howto/03-Tips/Tabs.html",{loader:()=>u(()=>import("./Tabs.html-CBnV8yex.js"),__vite__mapDeps([22,1])),meta:{d:1634225909e3,e:` +@startuml
과@enduml
사이에 UML 구성을 위한 구성을 넣어 표기합니다. 아래와 같이 md 파일 내에 작성하면컨텐츠에 탭을 추가하여 상황에 따라 선택적으로 문서를 읽을 수 있도록 합니다.
`,r:{minutes:.4,words:119},y:"a",t:"Tabs"}}],["/00-Howto/03-Tips/TipBox.html",{loader:()=>u(()=>import("./TipBox.html-DpUp3CBK.js"),__vite__mapDeps([23,1])),meta:{d:1634225909e3,e:` +
+상세 내용은Markdown Enhance
의 Tabs, Code Tabs 를 확인해보세요.문서 작성시 팁과 주의사항을 표기하는 방법을 설명합니다.
+
+공식 문서기본 사용법
+`,r:{minutes:.44,words:133},y:"a",t:"Tip Box"}}],["/01-Infrastructure/Container/container_runtime_sheet.html",{loader:()=>u(()=>import("./container_runtime_sheet.html-CjV2Qyyt.js"),__vite__mapDeps([24,1])),meta:{d:1640262e6,g:["container","docker","podman"],e:` +::: tip +This is a tip +::: + +::: warning +This is a warning +::: + +::: danger +This is a dangerous warning +::: + +::: details +This is a details block, which does not work in IE / Edge +::: +
++update : 2021. 12. 23.
++ +
`,r:{minutes:.58,words:173},y:"a",t:"Container Runtimes 비교 표"}}],["/01-Infrastructure/Container/rancher-desktop-disk-resize-mac.html",{loader:()=>u(()=>import("./rancher-desktop-disk-resize-mac.html-c5Gx1GDU.js"),__vite__mapDeps([25,1])),meta:{d:1683621743e3,g:["rancher","docker","mac"],e:` ++ + + ++ CRI-O +Containerd CRI plugin +Docker Engine +gVisor CRI plugin +CRI-O Kata Containers ++ +sponsors +CNCF +CNCF +Docker Inc +Intel ++ +started +2016 +2015 +Mar 2013 +2015 +2017 ++ +version +1.23 +1.19 +20.10 +release-20211129.0 +1.13 ++ +runtime +runc (default) +containerd managing runc +runc +runsc +kata-runtime ++ +kernel +shared +shared +shared +partially shared +isolated ++ +syscall filtering +no +no +no +yes +no ++ +kernel blobs +no +no +no +no +yes ++ +footprint +- +- +- +- +30mb ++ +start time +<10ms +<10ms +<10ms +<10ms +<100ms ++ +io performance +host performance +host performance +host performance +slow +host performance ++ +network performance +host performance +host performance +host performance +slow (see comment) +close to host performance ++ +Docs +https://github.com/kubernetes-sigs/cri-o/ +https://github.com/containerd/cri +https://github.com/moby/moby +https://github.com/google/gvisor +https://github.com/kata-containers/runtime ++ +장점 +경량의 쿠버네티스 전용 Docker 데몬이 필요하지 않음 OpenShift의 기본 컨테이너 런타임 아마도 최고의 컨테이너 기본 런타임 +최신 Docker Engine과 함께 기본적으로 설치됨 Kubernetes는 ContainerD를 직접 사용할 수 있으며, Docker또한 동일한 호스트에서 직접 사용할 수도 있음 DockerD 데몬을 실행할 필요가 없음 +방대한 수의 사용자가 테스트하고 반복 한 가장 성숙한 런타임 seccomp, SELinux 및 AppArmor를 사용하여 강화할 수 있음 가장 빠른 시작 시간 메모리 사용량이 가장 적음 +gcloud appengine에서 고객 간의 격리 계층으로 사용함 상태를 저장하지 않는 웹 앱에 적합 표준 컨테이너에 두 개의 보안 계층을 추가함 +아마도 가장 안전한 옵션 보안에 대한 주요 절충안으로 오버헤드가 발생하는것은 그렇게 나쁘지 않은 것으로 보임 ++ + +단점 +Docker Engine이 같고 있는 동일한 보안 이슈를 가지고 있음 보안정책을 별도로 관리해야 함 +This is slightly newer as it has been through a few iterations of being installed differently. +Kubernetes는 CRI 플러그인 아키텍처로 이동하고 있음 보안을 강화하고 관리하는것은 너무 복잡함 +버전이 지정되지 않았으며 아직 Kubernetes에서 프로덕션에 사용해서는 안됨 많은 syscall을 만드는 응용 프로그램에는 적합하지 않음 400 개 Linux syscall이 모두 구현되어 일부 앱이 작동하지 않을 수 있음 (예 : postgres). +kata-runtime 자체는 v1이지만 이것이 Kubernetes 상에서 어떻게 준비 되어 있는지 확인이 필요 30MB 메모리 오버 헤드로 인한 비효율적 패킹 시작 시간 ++`,r:{minutes:.36,words:109},y:"a",t:"Rancher Desktop Disk Resize on MAC"}}],["/01-Infrastructure/Container/rancher-desktop-insecure-setup-mac.html",{loader:()=>u(()=>import("./rancher-desktop-insecure-setup-mac.html-CXvCoHFH.js"),__vite__mapDeps([26,1])),meta:{d:165217296e4,g:["rancher","docker","mac"],e:` +Private docker registry
+
+Rancher Desktop
+MacOS
+Src : https://slack-archive.rancher.com/t/8508077/on-my-m1-mac-i-ve-started-getting-this-error-and-it-wont-go-#3e8d178c-aee8-46e6-b4cc-094c2339cbaa++Private docker registry
+
+Rancher Desktop
+MacOS
+Setupinsecure-registries
issue
+`,r:{minutes:.53,words:158},y:"a",t:"Rancher Desktop Insecure Setup on MAC"}}],["/02-PrivatePlatform/OpenShift/deploying_specificnode_by_namespace.html",{loader:()=>u(()=>import("./deploying_specificnode_by_namespace.html-QxOq8fbf.js"),__vite__mapDeps([27,1])),meta:{d:1695042774e3,g:["openshift","ocp"],e:` +$ docker push 192.168.60.11:5000/example-python:1.0 +Error response from daemon: Get https://192.168.60.11:5000/v1/example-python: http: server gave HTTP response to HTTPS client +
++원문 : https://blog.openshift.com/deploying-applications-to-specific-nodes/
+Deployment나 Deployment Config에서 Nodeselect를 지정하는 방법 외에 Project 단위로 설정하는 방법을 설명합니다.
`,r:{minutes:.55,words:165},y:"a",t:"OpenShift 3.x - 프로젝트 별로 특정 노드에 배포하기"}}],["/02-PrivatePlatform/OpenShift/openshift3.11_custom_metric_with_jboss.html",{loader:()=>u(()=>import("./openshift3.11_custom_metric_with_jboss.html-Ct_0tbjB.js"),__vite__mapDeps([28,1])),meta:{d:1695042774e3,g:["openshift","ocp","jboss"],e:` +++Autoscaling applications using custom metrics on OpenShift Container Platform 3.11 with JBoss EAP or Wildfly
+Red Hat OpenShift Container Platform 3.11 (OCP) 은 기본적으로 CPU에 대한 애플리케이션 자동 확장을 지원합니다. 추가적으로
`,r:{minutes:6.43,words:1930},y:"a",t:"OpenShift 3.11 - Custom metric with JBoss"}}],["/02-PrivatePlatform/Vsphere/Vsphere_template_issue.html",{loader:()=>u(()=>import("./Vsphere_template_issue.html-8QDvgHPa.js"),__vite__mapDeps([29,1])),meta:{d:1695042774e3,g:["vsphere","template"],e:` +apis/autoscaling/v2beta1
를 활성화하여 Memory의 메트릭을 기반으로 한 기능도 지원 합니다. CPU나 Memory의 경우 애플리케이션에 종속되지 않은 기본적인 메트릭이나, 때로는 추가적인 메트릭 요소를 기반으로 확장할 필요성이 있습니다.+
+- redhat 계열
++
+- 아래 네개의 패키지의 설치가 필요하다. 특히 perl은 거의 설치가 안되어 있음
+- open-vm-tools, open-vm-tools-deploypkg, net-tools, perl
+- 설치 후 template 생성하고 배포하면 됨
++
+- debian 계열
++
`,r:{minutes:.28,words:83},y:"a",t:"VSphere 템플릿 생성 이슈"}}],["/03-PublicCloud/AlibabaCloud/CredentialConfig.html",{loader:()=>u(()=>import("./CredentialConfig.html-B_-qDT_l.js"),__vite__mapDeps([30,1])),meta:{d:1695042774e3,g:["alibaba","aliyun"],e:` +- /etc/systemd/system/vmtoolsd.service 파일에 구문 추가
+- 18.04은 추가하여도 가끔 NIC, hostname이 기존에 템플릿의 정보를 가져올때가 있음
+1. CLI 설치
+1.1 Download 방식
++
`,r:{minutes:1.14,words:342},y:"a",t:"Alibaba CLI 설정"}}],["/03-PublicCloud/AlibabaCloud/",{loader:()=>u(()=>import("./index.html-Bk0z2AFD.js"),__vite__mapDeps([31,1])),meta:{d:1695042774e3,e:` +`,r:{minutes:.01,words:4},y:"a",t:"Alibaba Cloud"}}],["/03-PublicCloud/Azure/",{loader:()=>u(()=>import("./index.html-CierunXN.js"),__vite__mapDeps([32,1])),meta:{d:1695042774e3,e:` +`,r:{minutes:.01,words:3},y:"a",t:"Azure"}}],["/03-PublicCloud/NCP/",{loader:()=>u(()=>import("./index.html-CIUsUagt.js"),__vite__mapDeps([33,1])),meta:{d:1695042774e3,e:` +`,r:{minutes:.01,words:4},y:"a",t:"NCP(Naver Cloud Platform)"}}],["/06-etc/class/devops-discussion-1st.html",{loader:()=>u(()=>import("./devops-discussion-1st.html--IXABsuM.js"),__vite__mapDeps([34,1])),meta:{d:1640262e6,g:["devops","container"],e:` +- Install guide : https://partners-intl.aliyun.com/help/doc-detail/139508.htm
+- Release Download Page : https://github.com/aliyun/aliyun-cli/releases +
++
+- CLI 릴리즈 페이지에서 OS에 맞는 파일을 다운로드하여 사용
+일시 : 2019년 4월 24일 수요일 저녁 19시 ~ 21시
+안내 : 컨테이너 연구소 - 컨테이너 시스템의 활용 방향 및 미래에 관련해서 좌담
+장소 : 메가존 지하 강연장
+Q. 컨테이너란 무엇일까?
++
`,r:{minutes:.17,words:51},y:"a",t:"DevOps 연구소 좌담회 (1차)"}}],["/06-etc/class/devops-discussion-20240213.html",{loader:()=>u(()=>import("./devops-discussion-20240213.html-B8CuZjzi.js"),__vite__mapDeps([35,1])),meta:{a:"GS",d:1707896358e3,g:["devops","ai"],e:` +- +
+자원을 잘 나눠주는 프로세스.
+- +
+Zip같은 패키지인데 바퀴도 있고 엔진도 있는
+- +
+개발자들의 공용어
+- +
+VM이 H/W와의 분리였다면 컨테이너는 OS와 분리
+- +
+떠나보낸 연인...하지만 사랑한다
+ +DevOps Korea 좌담회 2024.2.13.
++
`,r:{minutes:.96,words:289},y:"a",t:"DevOps Korea 좌담회 2024.2.13."}}],["/06-etc/class/devops-discussion-2nd.html",{loader:()=>u(()=>import("./devops-discussion-2nd.html-CotdaWQ0.js"),__vite__mapDeps([36,1])),meta:{d:1640262e6,g:["devops","container"],e:` +- +
+일시 : 2024년 2월 13일 (화) 19:00
+- +
+안내 :
++
+- Facebook : DevOps Korea - AI의 시대에서 DevOps가 가야할 방향
+- GitHub : https://github.com/ralfyang/DevOps_Korea_sitting_talking/tree/main/20240213
+- +
+장소 : 파크원2타워 티오더
+- +
+주요 아젠다
++
+- AI시대 관련 변화된 시대에 맞게 대응 할 방향
+- DevOps 업무에 도움이 될만한 AI도구 및 방법론
+- 이미 적용중인 AI를 활용한 엔지니어링 업무소개 등
+일시 : 2019년 5월 23일 (목) 19:00 ~ 21:30
+안내 : 컨테이너 연구소 - 컨테이너 시스템의 활용 방향 및 미래에 관련해서 좌담 part2
+장소 : 대륭서초타워 베스핀글로벌
+Q. 컨테이너란?
++
`,r:{minutes:.34,words:103},y:"a",t:"DevOps 연구소 좌담회 (2차)"}}],["/06-etc/infomation/Keyboard-Eng.html",{loader:()=>u(()=>import("./Keyboard-Eng.html-CK_we2gb.js"),__vite__mapDeps([37,1])),meta:{d:1695042774e3,g:["keyboard","tip"],e:` +- Namespace가 지원되는 Process (Tech 관점)
+- 스타트업에서는 비용 절감 가능 ($ 관점)
++ +`,r:{minutes:.52,words:156},y:"a",t:"키보드의 특수기호 영어 명칭"}}],["/06-etc/infomation/acronyms.html",{loader:()=>u(()=>import("./acronyms.html-CIiKRHMZ.js"),__vite__mapDeps([38,1])),meta:{d:1695042774e3,g:["acronyms","tip"],e:` +++Full name definition
+
+List of informationtechnology initialismsA
++
`,r:{minutes:1.82,words:546},y:"a",t:"약어"}}],["/06-etc/mac/brew-cert-issue.html",{loader:()=>u(()=>import("./brew-cert-issue.html-yO1nxQHI.js"),__vite__mapDeps([39,1])),meta:{d:1640262179e3,g:["mac","homebrew","brew"],e:` +- ACL : Access Control List
+- AD : Active Directory
+- AES : Advanced Encryption Standard
+- AJAX : Asynchronous JavaScript and XML
+- API : Application Programming Interface
+- ARP : Address Resolution Protocol
+- AWS : Amazon Web Service
+- APM : Application Performance Monitoring(Management)
++
+- 현상 : brew 설치시 인증서 에러 발생
++ +`,r:{minutes:.66,words:198},y:"a",t:"homebrew install - certificate has expired"}}],["/06-etc/mac/libunistring-issue.html",{loader:()=>u(()=>import("./libunistring-issue.html-D9OVyvgy.js"),__vite__mapDeps([40,1])),meta:{d:1675205792e3,g:["mac","homebrew","brew","wget"],e:` +현상
+macOS Ventura 업그레이드 후 wget 실행시 오류 발생
+`,r:{minutes:.93,words:280},y:"a",t:"Library not loaded: libunistring.2.dylib"}}],["/06-etc/nodejs/node-sass.html",{loader:()=>u(()=>import("./node-sass.html-CEK1jTCc.js"),__vite__mapDeps([41,1])),meta:{d:1667618041e3,g:["arm","nodejs"],e:` +$ wget +dyld[4414]: Library not loaded: /usr/local/opt/libunistring/lib/libunistring.2.dylib + Referenced from: <1ECBA17E-A426-310D-9902-EFF0D9E10532> /usr/local/Cellar/wget/1.21.3/bin/wget + Reason: tried: '/usr/local/opt/libunistring/lib/libunistring.2.dylib' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/usr/local/opt/libunistring/lib/libunistring.2.dylib' (no such file), '/usr/local/opt/libunistring/lib/libunistring.2.dylib' (no such file), '/usr/local/lib/libunistring.2.dylib' (no such file), '/usr/lib/libunistring.2.dylib' (no such file, not in dyld cache), '/usr/local/Cellar/libunistring/1.1/lib/libunistring.2.dylib' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/usr/local/Cellar/libunistring/1.1/lib/libunistring.2.dylib' (no such file), '/usr/local/Cellar/libunistring/1.1/lib/libunistring.2.dylib' (no such file), '/usr/local/lib/libunistring.2.dylib' (no such file), '/usr/lib/libunistring.2.dylib' (no such file, not in dyld cache) +[1] 4414 abort wget +
aarch64에서 vuepress 실행을 위해 테스트를 하던 도중 node-gyp와 node-sass에 대한 오류를 맞이하게 되었다.
+node-sass의 경우 arm환경에 대한 빌드 릴리즈가 없는 관계로
+npm install
을 실행하면 다시 빌드를 하게되는데, 이때 node-sass를 빌드하는 과정에서 빌드 실패가 발생함node-sass란?
+node환경에서 sass는 css 코드로 변환해주는 스타일 전처리언어이다. c/c++로 되어있는 구성요소로 인해 빠른 빌드 속도를 제공한다.
`,r:{minutes:.39,words:118},y:"a",t:"node-sass와 sass로의 전환"}}],["/01-Infrastructure/Linux/TroubleShooting/Oom_killer.html",{loader:()=>u(()=>import("./Oom_killer.html-Cah9Qu1H.js"),__vite__mapDeps([42,1])),meta:{d:1640764183e3,g:["linux","oom","oom_killer"],e:` +OOM Killer의 주요 업무는 다음 두 가지입니다.
++
+- 실행 중인 모든 프로세스를 살펴보며 각 프로세스의 메모리 사용량에 따라 OOM 점수를 산출합니다.
+- OS에서 메모리가 더 필요하면 점수가 가장 높은 프로세스를 종료시킵니다.
+각 프로세스의 oom_score 관련 정보는 /proc/(pid) 디렉토리 하위에서 찾을 수 있습니다.
++
`,r:{minutes:.33,words:100},y:"a",t:"OOM Killer가 일하는 방식"}}],["/01-Infrastructure/Linux/TroubleShooting/SSH%20Too%20many%20authentication%20failures.html",{loader:()=>u(()=>import("./SSH Too many authentication failures.html-BcSkL8WN.js"),__vite__mapDeps([43,1])),meta:{d:1628085698e3,g:["linux","ssh"],e:` +- oom_adj (oom_adjust)
+- oom_score_adj
+- oom_score
+직역하자면
+너무많은 인증 실패로 인한 SSH 접속이 안된다.
는 메시지를 간혹 보게되는 경우가 있다.`,r:{minutes:.39,words:117},y:"a",t:"SSH Too many authentication failures"}}],["/01-Infrastructure/Linux/TroubleShooting/docker_bridge_netstat.html",{loader:()=>u(()=>import("./docker_bridge_netstat.html-DPHuUA76.js"),__vite__mapDeps([44,1])),meta:{d:1639634821e3,g:["linux","docker","bridge","netstat"],e:` +$ ssh myserver +Received disconnect from 192.168.0.43 port 22:2: Too many authentication failures +Connection to 192.168.0.43 closed by remote host. +Connection to 192.168.0.43 closed. +
+
+- 단순 netstat만으로 bridge모드로 기동된 docker의 port를 체크할 수 없다
+- 그래서 아래와 같은 절차가 필요하다.
+먼저 찾으려는 컨테이너의 port를 확인한다. (nomad로 배포되어 있는 컨테이너임)
+`,r:{minutes:1.06,words:317},y:"a",t:"docker나 nomad를 이용하여 bridge모드로 기동된 컨테이너의 port 체크"}}],["/02-PrivatePlatform/Kubernetes/01-Information/Kubernetes_scheduler.html",{loader:()=>u(()=>import("./Kubernetes_scheduler.html-DMN8z5Cq.js"),__vite__mapDeps([45,1])),meta:{d:1695042774e3,g:["kubernetes","scheduler","알고리즘"],e:` +nomad alloc status d78d5b32 +ID = d78d5b32-00c3-5468-284a-8c201058c53a +Eval ID = c6c9a1d9 +Name = 08_grafana.08_grafana[0] +Node ID = e11b7729 +Node Name = slave1 +Job ID = 08_grafana +Job Version = 0 +Client Status = running +Client Description = Tasks are running +Desired Status = run +Desired Description = <none> +Created = 18h42m ago +Modified = 2h36m ago + +Allocation Addresses (mode = "bridge") +Label Dynamic Address +*http yes 10.0.0.161:25546 +*connect-proxy-grafana yes 10.0.0.161:29382 -> 29382 +
+
`,r:{minutes:.65,words:196},y:"a",t:"kubernetes 스케쥴러 알고리즘"}}],["/02-PrivatePlatform/Kubernetes/02-Config/kubernetes_with_out_docker.html",{loader:()=>u(()=>import("./kubernetes_with_out_docker.html-DUkVpTlq.js"),__vite__mapDeps([46,1])),meta:{d:1695042774e3,g:["kubernetes","docker아님","containerd"],e:` +- 원본: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-scheduling/scheduler_algorithm.md
+- 예약되지 않은 각 포드에 대해 Kubernetes 스케줄러는 규칙 집합에 따라 클러스터에서 노드를 찾으려고합니다. Kubernetes 스케줄러에 대한 일반적인 소개는 scheduler.md 에서 찾을 수 있습니다 . 이 문서에서는 포드의 노드를 선택하는 방법에 대한 알고리즘을 설명합니다. 포드의 대상 노드를 선택하기 전에 두 단계가 있습니다. 첫 번째 단계는 모든 노드를 필터링하고 두 번째 단계는 나머지 노드의 순위를 매겨 포드에 가장 적합한 것을 찾는 것입니다.
++
+- docker가 없어도 k8s를 올릴 수 있다!
+`,r:{minutes:.79,words:237},y:"a",t:"containerd를 런타임으로 사용한 Kubernetes 설치"}}],["/02-PrivatePlatform/Kubernetes/02-Config/vagrant_k8s.html",{loader:()=>u(()=>import("./vagrant_k8s.html-BzqLHCKE.js"),__vite__mapDeps([47,1])),meta:{d:1695042774e3,g:["kubernetes","vagrant","docker","install"],e:` + +# 먼저 설치하여 환경파일을 가져오고 원하는 버전을 설치한다. +sudo apt-get install containerd -y + +sudo mkdir -p /etc/containerd + +containerd config default | sudo tee /etc/containerd/config.toml + +sudo systemctl stop containerd + +curl -LO https://github.com/containerd/containerd/releases/download/v1.4.4/containerd-1.4.4-linux-amd64.tar.gz + +tar xvf containerd-1.4.4-linux-amd64.tar.gz + +rm containerd-1.4.4-linux-amd64.tar.gz + +sudo cp bin/* /usr/bin/ + +sudo systemctl start containerd + +rm -rf bin + +sudo systemctl status containerd --lines 1 + +# k8s 설치시작 +curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add + +sudo apt-add-repository "deb http://apt.kubernetes.io/ kubernetes-xenial main" + +sudo apt-get install kubeadm kubelet kubectl -y + +sudo apt-mark hold kubeadm kubelet kubectl containerd + +#echo 'net.bridge.bridge-nf-call-iptables = 1' | sudo tee -a /etc/sysctl.conf + +SOURCE_FILE="/etc/sysctl.conf" +LINE_INPUT="net.bridge.bridge-nf-call-iptables = 1" + +grep -qF "$LINE_INPUT" "$SOURCE_FILE" || echo "$LINE_INPUT" | sudo tee -a "$SOURCE_FILE" + +sudo echo '1' | sudo tee /proc/sys/net/ipv4/ip_forward + +cat /proc/sys/net/ipv4/ip_forward + +sudo sysctl --system + +sudo modprobe overlay +sudo modprobe br_netfilter + +sudo swapoff -a + +sudo sed -ri '/\\sswap\\s/s/^#?/#/' /etc/fstab + +cat /etc/fstab +
+ +`,r:{minutes:1.78,words:535},y:"a",t:"Kubernetes, Vagrant로 로컬 환경 구성"}}],["/02-PrivatePlatform/Kubernetes/05-Kops/01-kops-on-aws.html",{loader:()=>u(()=>import("./01-kops-on-aws.html-ROAC0cUk.js"),__vite__mapDeps([48,1])),meta:{d:1695042774e3,g:["Kubernetes","Kops","EKS","PKOS"],e:` +++💡 본 글은 PKOS(Production Kubernetes Online Study) 2기 스터디의 일부로 작성된 내용입니다.
+
+실제 Production Kubernetes 환경에서 활용 가능한 다양한 정보를 전달하기 위한 시리즈로 작성 예정입니다.1. 실습환경 사전준비
+본 스터디는 AWS 환경에서 Kops(Kubernetes Operations)를 활용한 실습으로 진행할 예정입니다.
++`,r:{minutes:3.52,words:1055},y:"a",t:"[PKOS] 1편 - AWS Kops 설치 및 기본 사용"}}],["/02-PrivatePlatform/Kubernetes/05-Kops/02-kops-network-storage.html",{loader:()=>u(()=>import("./02-kops-network-storage.html-CvfHSXZ8.js"),__vite__mapDeps([49,1])),meta:{d:1695042774e3,g:["Kubernetes","Kops","EKS","PKOS"],e:` +📌 참고 : 필자는 개인적인 이유로 Route 53 계정과, kOps 클러스터 운영 계정을 나눠서 진행합니다.
+
+하나의 계정에서 실습을 진행할 경우에는 사전 환경구성이 다를 수 있는 점 참고 부탁드립니다.지난 1주차 스터디에이어 2주차 스터디를 진행하였습니다. 이번 스터디에서는 "쿠버네티스 네트워크" 및 "쿠버네티스 스토리지"를 중심으로 학습하였습니다.
+++참고 :
+
+원활한 실습을 위해 인스턴스 타입을 변경한 후 진행합니다.0. 사전준비
+1) Kops 클러스터의 인스턴 그룹 변경
+`,r:{minutes:7.23,words:2169},y:"a",t:"[PKOS] 2편 - 네트워크 & 스토리지"}}],["/02-PrivatePlatform/Kubernetes/06-EKS/01-eks-deploy.html",{loader:()=>u(()=>import("./01-eks-deploy.html-9Qn8CqOo.js"),__vite__mapDeps([50,1])),meta:{d:1695042774e3,g:["Kubernetes","EKS","PKOS"],e:` +kops get ig +NAME ROLE MACHINETYPE MIN MAX ZONES +master-ap-northeast-2a Master t3.medium 1 1 ap-northeast-2a +nodes-ap-northeast-2a Node t3.medium 1 1 ap-northeast-2a +nodes-ap-northeast-2c Node t3.medium 1 1 ap-northeast-2c +
이번에 연재할 스터디는 AWS EKS Workshop Study (=AEWS)이다. AWS에서 공식적으로 제공되는 다양한 HOL 기반의 Workshop과 가시다님의 팀에서 2차 가공한 컨텐츠를 기반으로 진행한다.
+ +필자는 기본적인 스터디내용을 이번 시리즈에 연재할 예정이며, 추가적으로 HashiCorp의 Consul, Vault 등을 샘플로 배포하며 연동하는 내용을 조금씩 다뤄볼 예정이다.
`,r:{minutes:5.19,words:1556},y:"a",t:"AEWS 1주차 - Amzaon EKS 설치 및 기본 사용"}}],["/02-PrivatePlatform/Kubernetes/06-EKS/02-eks-networking.html",{loader:()=>u(()=>import("./02-eks-networking.html-C9T0s8D7.js"),__vite__mapDeps([51,1])),meta:{d:1695042774e3,g:["Kubernetes","EKS","PKOS"],e:` +이번에 연재할 스터디는 AWS EKS Workshop Study (=AEWS)이다. AWS에서 공식적으로 제공되는 다양한 HOL 기반의 Workshop과 가시다님의 팀에서 2차 가공한 컨텐츠를 기반으로 진행한다.
+ +0. 실습환경 준비
`,r:{minutes:4.48,words:1344},y:"a",t:"AEWS 2주차 - Amzaon EKS Networking"}}],["/02-PrivatePlatform/Kubernetes/06-EKS/03-eks-storage.html",{loader:()=>u(()=>import("./03-eks-storage.html-Y0k0NC6t.js"),__vite__mapDeps([52,1])),meta:{d:1695042774e3,e:` +`,r:{minutes:.02,words:5},y:"a",t:"AEWS 3주차 - Amzaon EKS Storage"}}],["/04-HashiCorp/01-Packer/01-Information/HCP_Packer_Intro.html",{loader:()=>u(()=>import("./HCP_Packer_Intro.html-BtpDpHuz.js"),__vite__mapDeps([53,1])),meta:{d:1647827749e3,g:["Packer","HCP","Terraform"],e:` +HashiCorp의 제품은 설치형과 더불어 SaaS 모델로도 사용가능한 모델이 제공됩니다. 여기에는 지금까지 Terraform Cloud, HCP Vault, HCP Consul 이 제공되었습니다. HCP는 HashiCorp Cloud Platform의 약자 입니다.
++
+- HCP : https://cloud.hashicorp.com/
+여기에 최근 HCP Packer가 공식적으로 GA(General Available)되었습니다. HashiCorp의 솔루션들에 대해서 우선 OSS(Open Source Software)로 떠올려 볼 수 있지만 기업을 위해 기능이 차별화된 설치형 엔터프라이즈와 더불어 클라우드형 서비스도 제공되고 있으며 향후 새로운 솔루션들이 추가될 전망입니다.
`,r:{minutes:1.25,words:376},y:"a",t:"HCP Packer 소개"}}],["/04-HashiCorp/01-Packer/05-SamplePkr/AlibabaCloud.html",{loader:()=>u(()=>import("./AlibabaCloud.html-Ddvy_KKt.js"),__vite__mapDeps([54,1])),meta:{d:1632808034e3,g:["Packer","Sample","Alibaba"],e:` +packer.pkr.hcl
++
+- +
vault()
는 vault 연동시 사용가능 : https://www.packer.io/docs/templates/hcl_templates/functions/contextual/vault`,r:{minutes:.96,words:289},y:"a",t:"Alibaba Cloud Packer Sample"}}],["/04-HashiCorp/01-Packer/05-SamplePkr/Azure.html",{loader:()=>u(()=>import("./Azure.html-DPMhwAjI.js"),__vite__mapDeps([55,1])),meta:{d:1632808034e3,g:["Packer","Sample","Azure"],e:` +# packer build -force . + +locals { + access_key = vault("/kv-v2/data/alicloud", "access_key") + secret_key = vault("/kv-v2/data/alicloud", "secret_key") +} + +variable "region" { + default = "ap-southeast-1" + description = "https://www.alibabacloud.com/help/doc-detail/40654.htm" +} + +source "alicloud-ecs" "basic-example" { + access_key = local.access_key + secret_key = local.secret_key + region = var.region + image_name = "ssh_otp_image_1_5" + source_image = "centos_7_9_x64_20G_alibase_20210623.vhd" + ssh_username = "root" + instance_type = "ecs.n1.tiny" + io_optimized = true + internet_charge_type = "PayByTraffic" + image_force_delete = true +} + +build { + sources = ["sources.alicloud-ecs.basic-example"] + + provisioner "file" { + source = "./files/" + destination = "/tmp" + } + +# Vault OTP + provisioner "shell" { + inline = [ + "cp /tmp/sshd /etc/pam.d/sshd", + "cp /tmp/sshd_config /etc/ssh/sshd_config", + "mkdir -p /etc/vault.d", + "cp /tmp/vault.hcl /etc/vault.d/vault.hcl", + "cp /tmp/vault-ssh-helper /usr/bin/vault-ssh-helper", + "/usr/bin/vault-ssh-helper -verify-only -config=/etc/vault.d/vault.hcl -dev", + "sudo adduser test", + "echo password | passwd --stdin test", + "echo 'test ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers", + "sudo sed -ie 's/SELINUX=enforcing/SELINUX=disabled /g' /etc/selinux/config" + ] + } + +# Apache + provisioner "shell" { + inline = [ + "sudo yum -y update", + "sleep 15", + "sudo yum -y update", + "sudo yum -y install httpd", + "sudo systemctl enable httpd", + "sudo systemctl start httpd", + "chmod +x /tmp/deploy_app.sh", + "PLACEHOLDER=${var.placeholder} WIDTH=600 HEIGHT=800 PREFIX=gs /tmp/deploy_app.sh", + # "sudo firewall-cmd --zone=public --permanent --add-port=80/tcp", + # "sudo firewall-cmd --reload", + ] + } +} + +variable "placeholder" { + default = "placekitten.com" + description = "Image-as-a-service URL. Some other fun ones to try are fillmurray.com, placecage.com, placebeard.it, loremflickr.com, baconmockup.com, placeimg.com, placebear.com, placeskull.com, stevensegallery.com, placedog.net" +} +
packer.pkr.hcl
++
+- +
vault()
는 vault 연동시 사용가능 : https://www.packer.io/docs/templates/hcl_templates/functions/contextual/vault`,r:{minutes:1.41,words:424},y:"a",t:"Azure Packer Sample"}}],["/04-HashiCorp/01-Packer/05-SamplePkr/GCP.html",{loader:()=>u(()=>import("./GCP.html-BxxzWfAX.js"),__vite__mapDeps([56,1])),meta:{d:1632809937e3,g:["Packer","Sample","GCP"],e:` +# packer init -upgrade . +# packer build -force . + +locals { + client_id = vault("/kv/data/azure", "client_id") + client_secret = vault("/kv/data/azure", "client_secret") + tenant_id = vault("/kv/data/azure", "tenant_id") + subscription_id = vault("/kv/data/azure", "subscription_id") + resource_group_name = var.resource_name + virtual_network_name = "kbid-d-krc-vnet-002" + virtual_network_subnet_name = "d-mgmt-snet-001" + virtual_network_resource_group_name = "kbid-d-krc-mgmt-rg" + timestamp = formatdate("YYYYMMDD_hhmmss", timeadd(timestamp(), "9h")) #생성되는 이미지 이름을 time 기반으로 생성 +} + +variable "placeholder" { + default = "placekitten.com" + description = "Image-as-a-service URL. Some other fun ones to try are fillmurray.com, placecage.com, placebeard.it, loremflickr.com, baconmockup.com, placeimg.com, placebear.com, placeskull.com, stevensegallery.com, placedog.net" +} + +# Basic example : https://www.packer.io/docs/builders/azure/arm#basic-example +# MS Guide : https://docs.microsoft.com/ko-kr/azure/virtual-machines/linux/build-image-with-packer +source "azure-arm" "basic-example" { + client_id = local.client_id + client_secret = local.client_secret + subscription_id = local.subscription_id + tenant_id = local.tenant_id + + # shared_image_gallery { + # subscription = local.subscription_id + # resource_group = "myrg" + # gallery_name = "GalleryName" + # image_name = "gs_pkr_\${local.timestamp}" + # image_version = "1.0.0" + # } + managed_image_resource_group_name = local.resource_group_name + managed_image_name = "${var.image_name}-${local.timestamp}" + + os_type = "Linux" + # az vm image list-publishers --location koreacentral --output table + image_publisher = "RedHat" + # az vm image list-offers --location koreacentral --publisher RedHat --output table + image_offer = "RHEL" + # az vm image list-skus --location koreacentral --publisher RedHat --offer RHEL --output table + image_sku = "8_4" + + azure_tags = { + dept = "KBHC Terraform POC" + } + + # az vm list-skus --location koreacentral --all --output table + build_resource_group_name = local.resource_group_name + + ######################################### + # 기존 생성되어있는 network 를 사용하기 위한 항목 # + ######################################### + virtual_network_name = local.virtual_network_name + virtual_network_subnet_name = local.virtual_network_subnet_name + virtual_network_resource_group_name = local.virtual_network_resource_group_name + + # location = "koreacentral" + vm_size = "Standard_A2_v2" +} + +build { + sources = ["sources.azure-arm.basic-example"] + + provisioner "file" { + source = "./files/" + destination = "/tmp" + } + +# Vault OTP + provisioner "shell" { + inline = [ + "sudo cp /tmp/sshd /etc/pam.d/sshd", + "sudo cp /tmp/sshd_config /etc/ssh/sshd_config", + "sudo mkdir -p /etc/vault.d", + "sudo cp /tmp/vault.hcl /etc/vault.d/vault.hcl", + "sudo cp /tmp/vault-ssh-helper /usr/bin/vault-ssh-helper", + "echo \\"=== Vault_Check ===\\"", + "curl http://10.0.9.10:8200", + "/usr/bin/vault-ssh-helper -verify-only -config=/etc/vault.d/vault.hcl -dev", + "echo \\"=== Add User ===\\"", + "sudo adduser jboss", + "echo password | sudo passwd --stdin jboss", + "echo 'jboss ALL=(ALL) NOPASSWD: ALL' | sudo tee -a /etc/sudoers", + "echo \\"=== SELINUX DISABLE ===\\"", + "sudo sed -ie 's/SELINUX=enforcing/SELINUX=disabled /g' /etc/selinux/config" + ] + } + +# Apache + provisioner "shell" { + inline = [ + "sudo yum -y update", + "sleep 15", + "sudo yum -y update", + "sudo yum -y install httpd", + "sudo systemctl enable httpd", + "sudo systemctl start httpd", + "chmod +x /tmp/deploy_app.sh", + "sudo PLACEHOLDER=${var.placeholder} WIDTH=600 HEIGHT=800 PREFIX=gs /tmp/deploy_app.sh", + "sudo firewall-cmd --zone=public --permanent --add-port=80/tcp", + "sudo firewall-cmd --reload", + ] + } +} +
packer.pkr.hcl
+`,r:{minutes:.49,words:148},y:"a",t:"Google Cloud Platform Packer Sample"}}],["/04-HashiCorp/01-Packer/05-SamplePkr/aws-ubuntu.html",{loader:()=>u(()=>import("./aws-ubuntu.html-qEdr7imC.js"),__vite__mapDeps([57,1])),meta:{d:1639995661e3,g:["Packer","Sample","aws"],e:` +variable "base_image" { + default = "ubuntu-1804-bionic-v20210415" +} +variable "project" { + default = "gs-test-282101" +} +variable "region" { + default = "asia-northeast2" +} +variable "zone" { + default = "asia-northeast2-a" +} +variable "image_name" { + +} +variable "placeholder" { + default = "placekitten.com" + description = "Image-as-a-service URL. Some other fun ones to try are fillmurray.com, placecage.com, placebeard.it, loremflickr.com, baconmockup.com, placeimg.com, placebear.com, placeskull.com, stevensegallery.com, placedog.net" +} + +source "googlecompute" "basic-example" { + project_id = var.project + source_image = var.base_image + ssh_username = "ubuntu" + zone = var.zone + disk_size = 10 + disk_type = "pd-ssd" + image_name = var.image_name +} + +build { + name = "packer" + source "sources.googlecompute.basic-example" { + name = "packer" + } + + provisioner "file"{ + source = "./files" + destination = "/tmp/" + } + + provisioner "shell" { + inline = [ + "sudo apt-get -y update", + "sleep 15", + "sudo apt-get -y update", + "sudo apt-get -y install apache2", + "sudo systemctl enable apache2", + "sudo systemctl start apache2", + "sudo chown -R ubuntu:ubuntu /var/www/html", + "chmod +x /tmp/files/*.sh", + "PLACEHOLDER=${var.placeholder} WIDTH=600 HEIGHT=800 PREFIX=gs /tmp/files/deploy_app.sh", + ] + } +} +
ubuntu.pkr.hcl
+`,r:{minutes:.7,words:211},y:"a",t:"AWS Packer Sample - Ubuntu"}}],["/04-HashiCorp/01-Packer/05-SamplePkr/aws-windows.html",{loader:()=>u(()=>import("./aws-windows.html-p-W261cy.js"),__vite__mapDeps([58,1])),meta:{d:1639995661e3,g:["Packer","Sample","aws"],e:` +# packer init client.pkr.hcl +# packer build -force . + +variable "region" { + default = "ap-northeast-2" +} + +variable "cni-version" { + default = "1.0.1" +} + +packer { + required_plugins { + amazon = { + version = ">= 0.0.2" + source = "github.com/hashicorp/amazon" + } + } +} + +source "amazon-ebs" "example" { + ami_name = "gs_demo_ubuntu_{{timestamp}}" + instance_type = "t3.micro" + region = var.region + source_ami_filter { + filters = { + name = "ubuntu/images/*ubuntu-bionic-18.04-amd64-server-*" + root-device-type = "ebs" + virtualization-type = "hvm" + } + most_recent = true + owners = ["099720109477"] + } + ssh_username = "ubuntu" +} + +build { + sources = ["source.amazon-ebs.example"] + + provisioner "file" { + source = "./file/" + destination = "/tmp" + } + + provisioner "shell" { + inline = [ + "set -x", + "echo Connected via Consul/Nomad client at \\"${build.User}@${build.Host}:${build.Port}\\"", + "sudo apt-get update", + "sudo apt-get install -y apt-transport-https ca-certificates curl gnupg lsb-release", + "sudo apt-get update", + "curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -", + "sudo apt-add-repository \\"deb [arch=amd64] https://apt.releases.hashicorp.com bionic main\\"", + "sudo apt-get update && sudo apt-get -y install consul nomad netcat nginx", + "curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -", + "sudo add-apt-repository \\"deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable\\"", + "sudo apt-get update", + "sudo apt-get install -y docker-ce openjdk-11-jdk", + "curl -sL -o cni-plugins.tgz https://github.com/containernetworking/plugins/releases/download/v${var.cni-version}/cni-plugins-linux-amd64-v${var.cni-version}.tgz", + "sudo mkdir -p /opt/cni/bin", + "sudo tar -C /opt/cni/bin -xzf cni-plugins.tgz", + ] + } +} +
+ ++windows.pkr.hcl
+`,r:{minutes:1.84,words:551},y:"a",t:"AWS Packer Sample - Windows"}}],["/04-HashiCorp/01-Packer/05-SamplePkr/nCloud.html",{loader:()=>u(()=>import("./nCloud.html-DSp6DFO8.js"),__vite__mapDeps([59,1])),meta:{d:1632809475e3,g:["Packer","Sample","NCP"],e:` +variable "region" { + default = "ap-northeast-2" +} + +variable "cni-version" { + default = "1.0.1" +} + +locals { + nomad_url = "https://releases.hashicorp.com/nomad/1.2.3/nomad_1.2.3_windows_amd64.zip" + consul_url = "https://releases.hashicorp.com/consul/1.11.1/consul_1.11.1_windows_amd64.zip" + jre_url = "https://github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.13%2B8/OpenJDK11U-jre_x64_windows_hotspot_11.0.13_8.zip" +} + +packer { + required_plugins { + amazon = { + version = ">= 0.0.2" + source = "github.com/hashicorp/amazon" + } + } +} + +source "amazon-ebs" "example" { + ami_name = "gs_demo_windows_{{timestamp}}" + communicator = "winrm" + instance_type = "t2.micro" + region = var.region + source_ami_filter { + filters = { + name = "*Windows_Server-2019-English-Full-Base*" + root-device-type = "ebs" + virtualization-type = "hvm" + } + most_recent = true + owners = ["amazon"] + } + user_data_file = "./bootstrap_win.txt" + winrm_password = "SuperS3cr3t!!!!" + winrm_username = "Administrator" +} + +build { + sources = ["source.amazon-ebs.example"] + + provisioner "powershell" { + inline = [ + "New-Item \\"C:\\\\temp\\" -ItemType Directory", + ] + } + + // provisioner "file" { + // source = "./file/" + // destination = "/tmp" + // } + + provisioner "powershell" { + inline = [ + "New-Item \\"C:\\\\hashicorp\\\\jre\\\\\\" -ItemType Directory", + "New-Item \\"C:\\\\hashicorp\\\\consul\\\\bin\\\\\\" -ItemType Directory", + "New-Item \\"C:\\\\hashicorp\\\\consul\\\\data\\\\\\" -ItemType Directory", + "New-Item \\"C:\\\\hashicorp\\\\consul\\\\conf\\\\\\" -ItemType Directory", + "New-Item \\"C:\\\\hashicorp\\\\nomad\\\\bin\\\\\\" -ItemType Directory", + "New-Item \\"C:\\\\hashicorp\\\\nomad\\\\data\\\\\\" -ItemType Directory", + "New-Item \\"C:\\\\hashicorp\\\\nomad\\\\conf\\\\\\" -ItemType Directory", + "Invoke-WebRequest -Uri ${local.jre_url} -OutFile $env:TEMP\\\\jre.zip", + "Invoke-WebRequest -Uri ${local.consul_url} -OutFile $env:TEMP\\\\consul.zip", + "Invoke-WebRequest -Uri ${local.nomad_url} -OutFile $env:TEMP\\\\nomad.zip", + "Expand-Archive $env:TEMP\\\\jre.zip -DestinationPath C:\\\\hashicorp\\\\jre\\\\", + "Expand-Archive $env:TEMP\\\\consul.zip -DestinationPath C:\\\\hashicorp\\\\consul\\\\bin\\\\", + "Expand-Archive $env:TEMP\\\\nomad.zip -DestinationPath C:\\\\hashicorp\\\\nomad\\\\bin\\\\", + "[Environment]::SetEnvironmentVariable(\\"Path\\", $env:Path + \\";C:\\\\hashicorp\\\\jre\\\\jdk-11.0.13+8-jre\\\\bin;C:\\\\hashicorp\\\\nomad\\\\bin;C:\\\\hashicorp\\\\consul\\\\bin\\", \\"Machine\\")", + // "$old = (Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\\\\System\\\\CurrentControlSet\\\\Control\\\\Session Manager\\\\Environment' -Name path).path", + // "$new = \\"$old;C:\\\\hashicorp\\\\jre\\\\jdk-11.0.13+8-jre\\\\bin;C:\\\\hashicorp\\\\nomad\\\\bin;C:\\\\hashicorp\\\\consul\\\\bin\\"", + // "Set-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\\\\System\\\\CurrentControlSet\\\\Control\\\\Session Manager\\\\Environment' -Name path -Value $new", + ] + } +} +
packer.pkr.hcl
+`,r:{minutes:.36,words:108},y:"a",t:"Naver Cloud Platform Packer Sample"}}],["/04-HashiCorp/02-Vagrant/02-Config/multi-linux-sample.html",{loader:()=>u(()=>import("./multi-linux-sample.html-Kdb35Vof.js"),__vite__mapDeps([60,1])),meta:{d:162872854e4,g:["vagrant","virtualbox","linux"],e:` +packer { + required_plugins { + ncloud = { + version = ">= 0.0.1" + source = "github.com/hashicorp/ncloud" + } + } +} + +source "ncloud" "example-linux" { + access_key = var.access_key + secret_key = var.secret_key + server_image_product_code = "SPSW0LINUX000139" + server_product_code = "SPSVRGPUSSD00001" + server_image_name = var.image_name + server_image_description = "server image description" + region = "Korea" + communicator = "ssh" + ssh_username = "root" +} + +build { + sources = ["source.ncloud.example-linux"] + + provisioner "file" { + source = "jupyter.service" + destination = "/etc/systemd/system/jupyter.service" + } + + provisioner "shell" { + inline = [ + "yum clean all", + "yum -y install epel-release", + "yum -y install python3", + "yum -y install python-pip", + "pip3 install --upgrade pip", + "adduser jupyter", + "su - jupyter", + "pip3 install --user jupyter jupyter", + "systemctl enable jupyter", + "systemctl start jupyter" + ] + } +} + +variable "access_key" { + type = string +} + +variable "secret_key" { + type = string +} + +variable "image_name" { + type = string + default = "test" +} +
`,r:{minutes:.7,words:210},y:"a",t:"다양한 Linux 생성 샘플"}}],["/04-HashiCorp/02-Vagrant/04-TroubleShooting/hostonlynetworkissue.html",{loader:()=>u(()=>import("./hostonlynetworkissue.html-BPAGGOgU.js"),__vite__mapDeps([61,1])),meta:{d:1635125433e3,g:["vagrant","virtualbox"],e:` +# -*- mode: ruby -*- +# vi: set ft=ruby : + +# base image : https://app.vagrantup.com/bento +# Cluster IP have to set subnetting on private network subnet of VM + +$debianip = 50 +$centip = 60 +$suseip = 70 + +debian_cluster = { + "ubuntu" => { :image => "bento/ubuntu-18.04"} +} +cent_cluster = { + "centos" => { :image => "centos/7"}, + "rocky" => { :image => "rockylinux/8"}, +} +suse_cluster = { + "suse" => { :image => "opensuse/Tumbleweed.x86_64" } +} + +Vagrant.configure("2") do |config| + + config.vm.synced_folder '.', '/vagrant', disabled: true + + debian_cluster.each_with_index do |(hostname, info), i| + config.vm.define hostname do |server| + server.vm.box = info[:image] + server.vm.hostname = hostname + server.vm.network "private_network", name: "vboxnet1", ip: "172.28.128.#{i + $debianip}" + + server.vm.provider "virtualbox" do |v| + v.name = hostname + v.gui = false + v.memory = 1024 + v.cpus = 1 + + v.customize ["modifyvm", :id, "--vram", "9"] + end # end provider + end # end config + end # end cluster foreach + + suse_cluster.each_with_index do |(hostname, info), i| + config.vm.define hostname do |server| + server.vm.box = info[:image] + server.vm.hostname = hostname + server.vm.network "private_network", name: "vboxnet1", ip: "172.28.128.#{i + $suseip}" + server.vm.provider "virtualbox" do |v| + v.name = hostname + v.gui = false + v.memory = 1024 + v.cpus = 1 + + v.customize ["modifyvm", :id, "--vram", "9"] + end # end provider + end # end config + end # end cluster foreach + + cent_cluster.each_with_index do |(hostname, info), i| + config.vm.define hostname do |server| + server.vm.box = info[:image] + server.vm.hostname = hostname + server.vm.network "private_network", name: "vboxnet1", ip: "172.28.128.#{i + $centip}" + + server.vm.provider "virtualbox" do |v| + v.name = hostname + v.gui = false + v.memory = 1024 + v.cpus = 1 + + v.customize ["modifyvm", :id, "--vram", "9"] + end # end provider + end # end config + end # end cluster foreach + +end + +
https://discuss.hashicorp.com/t/vagrant-2-2-18-osx-11-6-cannot-create-private-network/30984/9
`,r:{minutes:.34,words:101},y:"a",t:"Network : Code E_ACCESSDENIED (0x80070005)"}}],["/04-HashiCorp/03-Terraform/01-Information/00-introduction.html",{loader:()=>u(()=>import("./00-introduction.html-BslrSW0n.js"),__vite__mapDeps([62,1])),meta:{d:1640262e6,g:["terraform","IaC"],e:` + +
+https://discuss.hashicorp.com/t/vagran-can-not-assign-ip-address-to-virtualbox-machine/309301. Provision
+프로비저닝과 관련하여 우리는 Day 0부터 Day 2까지의 여정이 있습니다.
++
`,r:{minutes:.69,words:207},y:"a",t:"Terraform 개념 소개"}}],["/04-HashiCorp/03-Terraform/01-Information/01-infrastructure_maturity.html",{loader:()=>u(()=>import("./01-infrastructure_maturity.html-CSXVrsyb.js"),__vite__mapDeps([63,1])),meta:{d:1640262e6,g:["terraform","IaC"],e:` + +- Day 0에 요구사항을 분석하고 아키텍쳐를 설계하고 훈련을 합니다.
+- Day 1에 드디어 설계된 아키텍쳐를 구현하지요. 인프라, 네트워크, 서비스 구성 등등 말이죠.
+- Day 2는 이제 Day 1에서 구성된 요소를 유지하고 관리하고 모니터링하면서 더나은 아키텍쳐로 변경하거나 추가 서비스를 붙이는 반복적 작업을 합니다.
+이번에는 인프라의 변화와 적응이라는 제목으로 인프라의 성숙도와 관련한 이야기를 나누고자 합니다.
+`,r:{minutes:.69,words:208},y:"a",t:"인프라의 변화와 적응"}}],["/04-HashiCorp/03-Terraform/01-Information/02-hcl.html",{loader:()=>u(()=>import("./02-hcl.html-DQHZhYcD.js"),__vite__mapDeps([64,1])),meta:{d:1640262e6,g:["terraform","usecase","IaC","HCL"],e:` +Terraform의 가장 주요한 기능으로 Infrastructure as Code 를 이야기 할 수 있습니다. 그리고 이를 지원하는 HCL에 대해 알아보고자 합니다.
+`,r:{minutes:.92,words:275},y:"a",t:"HCL - HashiCorp Configuration Language"}}],["/04-HashiCorp/03-Terraform/01-Information/remoteruns.html",{loader:()=>u(()=>import("./remoteruns.html-Dd5GtFY-.js"),__vite__mapDeps([65,1])),meta:{d:1640262e6,g:["terraform","IaC"],e:` +Terraform의 Remote Runs이라는 기능에 대해 확인합니다.
+Terraform Cloud와 Terraform Enterprise는 원격으로 트리거링 되어 동작하는 메커니즘을 제공하고 있습니다.
+`,r:{minutes:.82,words:247},y:"a",t:"Remote Runs"}}],["/04-HashiCorp/03-Terraform/01-Information/remotestate.html",{loader:()=>u(()=>import("./remotestate.html-CRXBKn_z.js"),__vite__mapDeps([66,1])),meta:{d:1640262e6,g:["terraform","IaC"],e:` +Terraform을 수행하고나면 실행되고난 뒤의 상태가 저장됩니다. 로컬에서 OSS로 실행 했을 때의
+`,r:{minutes:.6,words:179},y:"a",t:"Remote State"}}],["/04-HashiCorp/03-Terraform/01-Information/sentinel.html",{loader:()=>u(()=>import("./sentinel.html-BMW4Re1c.js"),__vite__mapDeps([67,1])),meta:{d:1640262e6,g:["terraform","IaC"],e:` +terraform.tfstate
파일이 그것 입니다. 서로 다른 팀이 각자의 워크스페이스에서 작업하고 난뒤 각 상태 공유하면 변경된 내역에 따라 다음 작업을 이어갈 수 있습니다. Terraform은 Terraform Cloud, HashiCorp Consul, Amazon S3, Alibaba Cloud OSS 등에 상태 저장을 지원합니다.Terraform은 인프라의 코드화 측면에서 그 기능을 충실히 실현해줍니다. 하지만 팀과 조직에서는 단지 인프라의 코드적 관리와 더불어 다른 기능들이 필요하기 마련입니다. 그중 하나로 정책을 꼽을 수 있습니다.
+`,r:{minutes:.47,words:140},y:"a",t:"Policy as Code : Sentinel"}}],["/04-HashiCorp/03-Terraform/01-Information/variables.html",{loader:()=>u(()=>import("./variables.html-CYnZOPtz.js"),__vite__mapDeps([68,1])),meta:{d:1640262e6,g:["terraform","IaC"],e:` +Terraform은 코드로 인프라를 관리하기위한 그 '코드'의 핵심 요소인 변수처리를 다양하게 지원합니다.
+ +Terraform에서는 다양한 변수와 작성된 변수를 관리하기 위한 메커니즘을 제공합니다. 가장 기본이되는 기능 중 하나이며 오픈소스와 엔터프라이즈 모두에서 사용가능합니다.
`,r:{minutes:.97,words:290},y:"a",t:"Variables"}}],["/04-HashiCorp/03-Terraform/01-Information/workspace.html",{loader:()=>u(()=>import("./workspace.html-DmVbFuEE.js"),__vite__mapDeps([69,1])),meta:{d:1640262e6,g:["terraform","IaC"],e:` +Terraform의 워크스페이스(Workspace)는 일종의 원하는 인프라의 프로비저닝 단위로서, 하나의 state를 갖는 공간입니다. Terraform에서의
+`,r:{minutes:.48,words:144},y:"a",t:"Workspace"}}],["/04-HashiCorp/03-Terraform/02-Config/TFEAdminPasswordReset.html",{loader:()=>u(()=>import("./TFEAdminPasswordReset.html-3khlDNqw.js"),__vite__mapDeps([70,1])),meta:{a:"jsp",d:1631870131e3,g:["terraform","admin","password"],e:` +plan
혹은apply
가 이뤄지는 단위이기도 합니다.Terraform Enterprise를 사용할 때, UI(https://TFE_SERVER) 상으로 접속할 수 없는 상황에서 비밀번호 변경이 필요한 경우, 아래와 같이 작업할 수 있다.
+Admin 계정의 경우
+다음과 같이 수정 가능.
+`,r:{minutes:.73,words:219},y:"a",t:"Terraform Enterprise 사용자 비밀번호 변경"}}],["/04-HashiCorp/03-Terraform/02-Config/terraform-cloud-agent-guide-custom.html",{loader:()=>u(()=>import("./terraform-cloud-agent-guide-custom.html-DJkAXNqy.js"),__vite__mapDeps([71,1])),meta:{d:1704702469e3,g:["Terraform"],e:` +# 이전 버전의 TFE +sudo docker exec -it ptfe_atlas /usr/bin/init.sh /app/scripts/wait-for-token -- bash -i -c 'cd /app && ./bin/rails c' +## 수정 최신 버전의 TFE에서는 Container 이름이 변경됨 (2022.6.21) +sudo docker exec -it tfe-atlas /usr/bin/init.sh /app/scripts/wait-for-token -- bash -i -c 'cd /app && ./bin/rails c' +
Terraform Cloud Agent(Agent)는 Terraform Enterprise/Cloud(TFE/C)에서 사용가능한 사용자 정의 Terraform 실행 환경을 제공합니다. 사용자는 Agent를 사용하여 Terraform 실행을 위해 기본 제공되는 이미지 대신 커스텀 패키지가 설치된 별도 이미지를 사용할 수 있고, 이미지 실행 위치를 네트워크 환경에서 자체 호스팅 할 수 있습니다.
+`,r:{minutes:1.74,words:522},y:"a",t:"Terraform Cloud Agent 가이드"}}],["/04-HashiCorp/03-Terraform/03-Sample/hashicat-azure.html",{loader:()=>u(()=>import("./hashicat-azure.html-D3ng3dBJ.js"),__vite__mapDeps([72,1])),meta:{d:168941454e4,g:["Terraform","Terraform on Azure","Azure","HashiCat","Terraform OSS","Terraform Cloud","Terraform Enterprise","Terraform 샘플","IaC"],e:` ++`,r:{minutes:4.18,words:1254},y:"a",t:"Intro to Terraform on Azure"}}],["/04-HashiCorp/03-Terraform/03-Sample/nomad-csi-sample.html",{loader:()=>u(()=>import("./nomad-csi-sample.html-1pYlYBZl.js"),__vite__mapDeps([73,1])),meta:{d:166454877e4,g:["Nomad","terrafom","CSI"],e:` +본 글은 HashiCorp의 공식 워크샵인 "Intro to Terraform on Azure" 내용을 발췌하여 작성한 글입니다. 참고
+실습 원본 소스코드는 hashicat-azure 저장소에서 확인할 수 있습니다.
++
`,r:{minutes:1.51,words:452},y:"a",t:"Nomad CSI Sample"}}],["/04-HashiCorp/03-Terraform/04-TroubleShooting/NotAllowAdminUsername.html",{loader:()=>u(()=>import("./NotAllowAdminUsername.html-BUoNYKC5.js"),__vite__mapDeps([74,1])),meta:{d:1632444868e3,g:["Terraform","Azure"],e:` +- AWS에 EFS를 Nomad CSI로 활용
+- full code는 아래 github를 참고 +
++
+- 참고 github: https://github.com/Great-Stone/nomad-demo-with-ecs +
++
+- branches: ung
++ +
`,r:{minutes:.52,words:156},y:"a",t:"The Admin Username specified is not allowed."}}],["/04-HashiCorp/03-Terraform/04-TroubleShooting/StateRemove.html",{loader:()=>u(()=>import("./StateRemove.html-DwYONtji.js"),__vite__mapDeps([75,1])),meta:{d:1642083602e3,g:["Terraform","State"],e:` ++ + + +Log ++ + +Error : compute.VirtualMachinesClient#CreateOrUpdate: Failure sending request: StatusCode=400 – Original Error: Code=“InvalidParameter” Message=“The Admin Username specified is not allowed.” Target="adminUsername" ++
`,r:{minutes:.39,words:116},y:"a",t:"State rm"}}],["/04-HashiCorp/03-Terraform/04-TroubleShooting/TFE_v202111-1(582)_Issue.html",{loader:()=>u(()=>import("./TFE_v202111-1(582)_Issue.html-BxHCp0t5.js"),__vite__mapDeps([76,1])),meta:{d:1640238674e3,g:["Terraform","Enterprise"],e:` +- +
+현상
+... googleapi: Error 400: Invalid request: Invalid request since instance is not running. +
: Terraform을 통하지 않고 리소스가 삭제되어, 해당 리소스를 찾지 못하는 상황 발생
+- +
+State 삭제
+Local 환경의 terraform에 remote를 Terraform cloud로 지정
+terraform { + required_version = ">= 0.12" + backend "remote" { + hostname = "app.terraform.io" + organization = "lguplus" + + workspaces { + name = "kids_library" + } + } +} +
state 리스트 확인
+terraform state list
my-workspace > terraform state list +random_pet.sql +module.Cluster_GKE.google_container_cluster.k8sexample +module.Cluster_GKE.google_container_node_pool.pool_1 +module.Cluster_GKE.google_container_node_pool.pool_2 +module.gcs_buckets.google_storage_bucket.buckets[0] +module.sql-db.google_sql_database.default +module.sql-db.google_sql_database_instance.default +module.sql-db.google_sql_user.default +module.sql-db.null_resource.module_depends_on +module.sql-db.random_id.user-password +module.network.module.routes.google_compute_route.route["egress-internet"] +module.network.module.subnets.google_compute_subnetwork.subnetwork["asia-northeast3/fc-kidslib-stg-subnet-1"] +module.network.module.vpc.google_compute_network.network +
존재하지 않는 resource를 삭제
+terraform state rm [resource_name]
my-workspace > terraform state rm module.sql-db +Removed module.sql-db.google_sql_database.default +Removed module.sql-db.google_sql_database_instance.default +Removed module.sql-db.google_sql_user.default +Removed module.sql-db.null_resource.module_depends_on +Removed module.sql-db.random_id.user-password +Successfully removed 5 resource instance(s). +
++v202111-1 (582) 버전 이상으로 설치, 또는 업그레이드 시 발생하는 이슈
+현상
+ ++ +
`,r:{minutes:.39,words:116},y:"a",t:"TFE Release v202111-1 (582) Issue"}}],["/04-HashiCorp/03-Terraform/04-TroubleShooting/error-state-snapshot-was-created-by-terraform-version.html",{loader:()=>u(()=>import("./error-state-snapshot-was-created-by-terraform-version.html-C61Znmv0.js"),__vite__mapDeps([77,1])),meta:{d:1695176473e3,g:["Terraform","Azure"],e:` ++ + + +Nginx access Log ++ + +2021/12/17 02:58:31 [error] 10#10: *913 connect(0mfailed (111: Connection refused) while connecting to upstream, client: 10.10.10.100, server:tfe.mydomain.com, reguest: "GET / HTTP/1.1", upstream: "http://172.11.0.1:9292/", host: "tfe.mydomain.com" ++ +
++ + + +Log ++ + +Error: state snapshot was created by Terraform v0.13.2, which is newer than current v0.12.26; upgrade to Terraform v0.13.2 or greater to work with this state ++
`,r:{minutes:.4,words:121},y:"a",t:"Error: state snapshot was created by Terraform vX.Y.Z"}}],["/04-HashiCorp/03-Terraform/04-TroubleShooting/re-install.html",{loader:()=>u(()=>import("./re-install.html-DTxZAjyz.js"),__vite__mapDeps([78,1])),meta:{a:"jsp",d:1655861356e3,g:["Terraform","Enterprise","TFE"],e:` +- 버전관련 에러 메시지에 대해 몇가지 테스트 해본 결과, 상위버전으로 Terraform State가 생성 된 이후 하위 버전으로 Refresh/Plan/Apply 를 수행하는 경우에 발생하는 것으로 확인
+- +
terraform_remote_state
는 버전에 관계 없이 워크스페이스 간에 output을 읽어올 수 있음을 확인+`,r:{minutes:.16,words:49},y:"a",t:"TFE 재설치 주의사항"}}],["/04-HashiCorp/03-Terraform/05-Airgap/ProviderBundling.html",{loader:()=>u(()=>import("./ProviderBundling.html-CNwkfDLV.js"),__vite__mapDeps([79,1])),meta:{d:165041363e4,g:["terraform","provider"],e:` +관련 Knowledge Base Article : https://support.hashicorp.com/hc/en-us/articles/4409044739859-Container-ptfe-base-startup-failed
+++https://github.com/hashicorp/terraform/tree/main/tools/terraform-bundle
+
+Terraform Enterprise에서 동작하는 기능입니다.Airgap 환경에서 사용할 특정 버전의 Terraform과 여러 제공자 플러그인을 모두 포함하는 zip 파일 인 "번들 아카이브"를 생성하는 툴을 사용합니다. 일반적으로 Terraform init을 통해 특정 구성 작업에 필요한 플러그인을 다운로드하고 설치하지만 Airgap 환경에서는 공식 플러그인 저장소에 액세스 할 수 없는 경우가 발생합니다. Bundle 툴을 사용하여 Terraform 버전과 선택한 공급자를 모두 설치하기 위해 대상 시스템에 압축을 풀 수있는 zip 파일이 생성되므로 즉석 플러그인 설치가 필요하지 않습니다.
`,r:{minutes:.85,words:256},y:"a",t:"Terraform Provider - 번들링"}}],["/04-HashiCorp/03-Terraform/05-Airgap/ProviderLocalFilesystem.html",{loader:()=>u(()=>import("./ProviderLocalFilesystem.html-XUIaiwWB.js"),__vite__mapDeps([80,1])),meta:{d:165041363e4,g:["terraform","provider"],e:` ++`,r:{minutes:1.34,words:401},y:"a",t:"Terraform Provider - 로컬 디렉토리"}}],["/04-HashiCorp/03-Terraform/05-Airgap/ProviderLocalMirroring.html",{loader:()=>u(()=>import("./ProviderLocalMirroring.html-C7POxPIe.js"),__vite__mapDeps([81,1])),meta:{d:165041363e4,g:["terraform","provider"],e:` +https://www.terraform.io/docs/cli/config/config-file.html#implied-local-mirror-directories
+
+https://learn.hashicorp.com/tutorials/terraform/provider-use?in=terraform/providers++https://www.terraform.io/docs/cli/config/config-file.html#provider_installation
+Terraform CLI를 사용할 때, 기본적으로 코드 상에서 사용하는 플러그인은 registry.terraform.io에서 다운로드 받게 되어 있습니다.
`,r:{minutes:.24,words:72},y:"a",t:"Terraform Provider - 로컬 미러링"}}],["/04-HashiCorp/04-Consul/01-Information/Consul%20Enterprise%20Feature.html",{loader:()=>u(()=>import("./Consul Enterprise Feature.html-BQm8HMfI.js"),__vite__mapDeps([82,1])),meta:{d:1628557352e3,g:["Consul","Enterprise"],e:` ++
`,r:{minutes:.5,words:151},y:"a",t:"Consul Enterprise Feature"}}],["/04-HashiCorp/04-Consul/01-Information/consul-sizing.html",{loader:()=>u(()=>import("./consul-sizing.html-BwUw-uKK.js"),__vite__mapDeps([83,1])),meta:{d:1642218029e3,g:["consul","sizing"],e:` +- Enterprise Global Visibility & Scale +
++
+- Network Segments
+- Advanced Federation
+- Redendancy Zones
+- Enganced Read Scalability
+- Governance & Policy +
++
+- Namespaces
+- Single Sign On
+- Audit Logging
+- Sentinel
++`,r:{minutes:.46,words:137},y:"a",t:"Consul Sizing"}}],["/04-HashiCorp/04-Consul/01-Information/port-info.html",{loader:()=>u(()=>import("./port-info.html-BSA3VqKM.js"),__vite__mapDeps([84,1])),meta:{d:164023929e4,g:["consul","port","requirement"],e:` +https://learn.hashicorp.com/tutorials/consul/reference-architecture
+Consul은 Server/Client 구조로 구성되며, Client의 경우 자원사용량이 매우 미미하므로 자원산정은 Server를 기준으로 산정
++ ++Consul 포트
+ +Port Table
++ +
`,r:{minutes:.31,words:92},y:"a",t:"Consul Port"}}],["/04-HashiCorp/04-Consul/02-Configuration/ForwardDns.html",{loader:()=>u(()=>import("./ForwardDns.html-w11anbby.js"),__vite__mapDeps([85,1])),meta:{d:1642495573e3,g:["Consul","Enterprise","Configuration","ForwardDns"],e:` ++ + + +Use +Default Ports ++ +DNS (TCP and UDP) +8600 ++ +HTTP (TCP Only) +8500 ++ +HTTPS (TCP Only) +disabled (8501)* ++ +gRPC (TCP Only) +disabled (8502)* ++ +LAN Serf (TCP and UDP) +8301 ++ +Wan Serf (TCP and UDP) +8302 ++ +server (TCP Only) +8300 ++ +Sidecar Proxy Min: 자동으로 할당된 사이드카 서비스 등록에 사용할 포함 최소 포트 번호 +21000 ++ + +Sidecar Proxy Max: 자동으로 할당된 사이드카 서비스 등록에 사용할 포괄적인 최대 포트 번호 +21255 +Consul dns를 local에서도 사용해야 할 경우에는 dns forward를 해줘야한다. 아래는 ubuntu 환경에서 진행하였음
+설정 명령어
+`,r:{minutes:.41,words:124},y:"a",t:"ForwardDns"}}],["/04-HashiCorp/04-Consul/02-Configuration/acl-sample.html",{loader:()=>u(()=>import("./acl-sample.html-CK60trmj.js"),__vite__mapDeps([86,1])),meta:{d:1648777606e3,g:["Consul","Acl","Policy"],e:`#systemd-resolved 설정파일 추가 및 변경 +mkdir -p /etc/systemd/resolved.conf.d +( +cat <<-EOF +[Resolve] +DNS=127.0.0.1 +DNSSEC=false +Domains=~consul +EOF +) | sudo tee /etc/systemd/resolved.conf.d/consul.conf +( +cat <<-EOF +nameserver 127.0.0.1 +options edns0 trust-ad +EOF +) | sudo tee /etc/resolv.conf +#iptables에 consul dns port 추가 +iptables --table nat --append OUTPUT --destination localhost --protocol udp --match udp --dport 53 --jump REDIRECT --to-ports 8600 +iptables --table nat --append OUTPUT --destination localhost --protocol tcp --match tcp --dport 53 --jump REDIRECT --to-ports 8600 +#service 재시작 +systemctl restart systemd-resolved +
Consul ACL Policy sample
+Consul ACL을 활성화 할 경우 default를 deny로 할 지 allow를 할 지 정할 수 있다.
+
+deny로 할 경우에는 하나하나 policy로 tokne을 만들어서 사용해야 한다.Consul이 Vault의 Storage로 되어야 할 경우
+`,r:{minutes:.23,words:69},y:"a",t:""}}],["/04-HashiCorp/04-Consul/02-Configuration/client.html",{loader:()=>u(()=>import("./client.html-DFcgtSZo.js"),__vite__mapDeps([87,1])),meta:{d:1629519876e3,g:["Consul","Enterprise","Configuration","Client"],e:` +key_prefix "vault/" { + policy = "write" +} +service "vault" { + policy = "write" +} +agent_prefix "" { + policy = "read" +} +session_prefix "" { + policy = "write" +} +
++팁
+최대한 설정값을 넣어보고, 번역기도 돌려보고 물어도 보고 넣은 Client설정 파일입니다.
+
+네트워크는 프라이빗(온프레이머스) 환경입니다.`,r:{minutes:.46,words:138},y:"a",t:"Consul 클라이언트 설정"}}],["/04-HashiCorp/04-Consul/02-Configuration/common.html",{loader:()=>u(()=>import("./common.html-DqrMHNmB.js"),__vite__mapDeps([88,1])),meta:{d:1629519254e3,g:["Consul","Enterprise","Configuration","Common"],e:` +#consul client 설정 +server = false + +acl = { + enabled = true + default_policy = "deny" + enable_token_persistence = true + tokens = { + agent = "f820514a-5215-e741-fcb3-c00857405230" + } +} + +license_path = "/opt/license/consul.license" + +retry_join = ["172.30.1.17","172.30.1.18","172.30.1.19"] + +rejoin_after_leave = true + + +#tls 설정 +ca_file = "/opt/ssl/consul/consul-agent-ca.pem" +auto_encrypt = { + tls = true +} + +verify_incoming = false +verify_outgoing = true +verify_server_hostname = true +
++팁
+최대한 설정값을 넣어보고, 번역기도 돌려보고 물어도 보고 넣은 server, client의 공통설정 파일입니다.
+
+저는 agent.hcl파일안에 다 넣고 실행하지만 나눠서 추후에는 기능별로 나눠서 사용할 예정입니다.`,r:{minutes:.56,words:168},y:"a",t:"Consul 공통 설정"}}],["/04-HashiCorp/04-Consul/02-Configuration/server.html",{loader:()=>u(()=>import("./server.html-BnjH4AJE.js"),__vite__mapDeps([89,1])),meta:{d:1629519523e3,g:["Consul","Enterprise","Configuration","Server"],e:` +#node name에는 _금지 +#node_name + +client_addr = "0.0.0.0" +bind_addr = "{{ GetInterfaceIP \`ens192\` }}" +advertise_addr = "{{ GetInterfaceIP \`ens224\` }}" + +#ipv4, ipv6를 나눠서 설정할 수 있음. +#advertise_addr_ipv4 +#advertise_addr_ipv6 + +ports { + #http = 8500 + http = -1 + dns = 8600 + #https = -1 + https = 8500 + serf_lan = 8301 + grpc = 8502 + server = 8300 +} + +#gossip ip 지정 +#serf_lan +#gossip 대역대 지정 +#serf_lan_allowed_cidrs + +#사용자 감사, 사용자가 consul에서 사용한 행동을 기록 +#audit { +# enabled = true +# sink "My sink" { +# type = "file" +# format = "json" +# path = "data/audit/audit.json" +# #consul의 감사작성방법 규칙, 현재는 best-effort만지원 +# delivery_guarantee = "best-effort" +# rotate_duration = "24h" +# rotate_max_files = 15 +# rotate_bytes = 25165824 +# } +#} + +#consul 서버관리 설정 변경 +#autopoilt { +# #새로운 서버가 클러스터에 추가될 때 죽은 서버 자동제거 +# cleanup_dead_servers = ture +# +# last_contact_threshold = 200ms +# #최소 quorm 수 지정 +# min_quorum = ni +# #클러스터에 서버가 추가될 시 안정상태로 되어야 하는 최소 시간 +# server_stabilization_time = 10s +#} + +#동시에 처리할 수 있는 인증서 서명 요청 제한 +#csr_max_concurrent = 0 +#서버가 수락할 인증서 서명 요청(CSR)의 최대 수에 대한 속도 제한을 설정 +#csr_max_per_second = 50 +#클러스터에서 이전 루트 인증서를 교체할 때 사용 +#leaf_cert_ttl = 72h +#CA 키 생성 타입 +#private_key_type = ec +#CA 키 생성될 길이 +#private_key_bits = 256 + +#서버에서만 client를 join할 수 있게 함 +#disable remote exec + +#enable syslog = true +log_level = "DEBUG" +data_dir = "/var/log/consul/consul" +log_file = "/var/log/consul/consul.log" +log_rotate_duration = "24h" +log_rotate_bytes = 104857600 +log_rotate_max_files = 100 + +license_path = "/opt/license/consul.license" + +acl { + enabled = true + default_policy = "allow" + enable_token_persistence = true + + #acl policy ttl, 줄이면 새로고침 빈도 상승, 성능에 영향을 미칠 수 있음 + #policy_ttl = 30s + #acl role ttl, 줄이면 새로고침 빈도 상승, 성능에 영향을 미칠 수 있음 + #role_ttl = 30s +} + +connect { + enabled = true + #vault 연동 옵션 + #ca_provider +} + +dns_config { + allow_stale = true, + max_stale = "87600h" +} + +#block_endpoints할성화시 restapi 차단 +#http_config { +# block_endpoints = false +#} + +#segments + +rpc { + enable_streaming = true +} + +encrypt = "7VY2fVm0p6vJUYNS/oex/mr2e59dy4AaGMefTKtUGi0=" +encrypt_verify_incoming = false +encrypt_verify_outgoing = false +
++팁
+최대한 설정값을 넣어보고, 번역기도 돌려보고 물어도 보고 넣은 server설정 파일입니다.
+
+네트워크는 프라이빗(온프레이머스) 환경입니다.`,r:{minutes:.59,words:177},y:"a",t:"Consul 서버 설정"}}],["/04-HashiCorp/04-Consul/03-UseCase/Consul%20Enterprise%20Feature.html",{loader:()=>u(()=>import("./Consul Enterprise Feature.html-I88Lp2Lj.js"),__vite__mapDeps([90,1])),meta:{d:1628557352e3,g:["Consul","Hybrid","Kubetenetes","k8s","VM"],e:` +#consul server 설정 +server = true +ui_config { + enabled = true +} +bootstrap_expect = 3 + +license_path = "/opt/license/consul.license" + +retry_join = ["172.30.1.17","172.30.1.18","172.30.1.19"] + +performance { + raft_multiplier = 1 +} + +#raft protocal 버전, consul 업데이트 시 1씩 증가 +raft_protocol = 3 + +#node가 완전히 삭제되는 시간 +reconnect_timeout = "72h" + +raft_snapshot_interval = "5s" + +#해당 서버를 non-voting server로 지정 +#read_replica = false + +limits { + http_max_conns_per_client = 200 + rpc_handshake_timeout = "5s" +} + +key_file = "/opt/ssl/consul/dc1-server-consul-0-key.pem" +cert_file = "/opt/ssl/consul/dc1-server-consul-0.pem" +ca_file = "/opt/ssl/consul/consul-agent-ca.pem" +auto_encrypt { + allow_tls = true +} + +verify_incoming = false, +verify_incoming_rpc = true +verify_outgoing = true +verify_server_hostname = false +
++이 문서에서는 Consul을 사용하여 상이한 두 Consul로 구성된 클러스터(마스터가 별개)의 서비스를 연계하는 방법을 설명합니다.
+1. 개요
+1.1 아키텍처
+네트워크 영역이 분리되어있는 두 환경의 애플리케이션 서비스들을 Service Mesh로 구성하는 방법을 알아 봅니다. 이번 구성 예에서는 Kubernetes와 Baremetal(BM)이나 VirtualMachine(VM)에 Consul Cluster(Datacenter)를 구성하고 각 환경의 애플리케이션 서비스를 Mesh Gateway로 연계합니다.
`,r:{minutes:8.05,words:2415},y:"a",t:"Consul Mesh Gateway - K8S x BMs/VMs"}}],["/04-HashiCorp/04-Consul/03-UseCase/Consul%20Health%20Check.html",{loader:()=>u(()=>import("./Consul Health Check.html-DGZ9Ux0W.js"),__vite__mapDeps([91,1])),meta:{a:"euimokna",d:1629704782e3,g:["Consul"],e:` ++`,r:{minutes:.23,words:70},y:"a",t:"Consul Health Check on VMs"}}],["/04-HashiCorp/04-Consul/04-TroubleShooting/Consul%20Enterprise%20Feature.html",{loader:()=>u(()=>import("./Consul Enterprise Feature.html-CWApFHx1.js"),__vite__mapDeps([92,1])),meta:{d:1628557352e3,g:["Consul"],e:` +https://www.consul.io/docs/discovery/services
+
+https://learn.hashicorp.com/tutorials/consul/service-registration-health-checks?in=consul/developer-discovery#tuning-scripts-to-be-compatible-with-consul+ +`,r:{minutes:.65,words:194},y:"a",t:"Identifying consul split-brain"}}],["/04-HashiCorp/04-Consul/04-TroubleShooting/Consul%20Install.html",{loader:()=>u(()=>import("./Consul Install.html-CqnmzIZZ.js"),__vite__mapDeps([93,1])),meta:{d:1645588712e3,g:["Consul","install"],e:` +AmazonLinux 환경에서 하기와 같은 명령어로 consul 설치 후 systemd 를 통한 Consul 시작시 오류 발생
+`,r:{minutes:.22,words:67},y:"a",t:"Consul yum install issue"}}],["/04-HashiCorp/04-Consul/04-TroubleShooting/Consul%20Sidecar%20Inject%20not%20working%20on%20k8s.html",{loader:()=>u(()=>import("./Consul Sidecar Inject not working on k8s.html-CDhmBQ6y.js"),__vite__mapDeps([94,1])),meta:{d:1628557352e3,g:["Consul","ServiceMesh","SideCar","Kubernetes","K8S"],e:` +sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo +sudo yum -y install consul +
++Consul Version : 1.9.x
+
+Helm Chart : 0.30.0Consul을 쿠버네티스 상에 구성하게 되면
+annotation
구성만으로도 쉽게 Sidecar를 애플리케이션과 함께 배포 가능하다.참고 : Controlling Injection Via Annotation
`,r:{minutes:.83,words:250},y:"a",t:"Consul Sidecar Inject not working on K8S"}}],["/04-HashiCorp/04-Consul/04-TroubleShooting/connection-termination.html",{loader:()=>u(()=>import("./connection-termination.html-DgTeN3Bf.js"),__vite__mapDeps([95,1])),meta:{d:1649167505e3,g:["Consul","ServiceMesh","SideCar","Kubernetes","K8S"],e:` ++ +`,r:{minutes:.42,words:126},y:"a",t:"Connection termination"}}],["/04-HashiCorp/04-Consul/05-Template_Sample/kv-sample.html",{loader:()=>u(()=>import("./kv-sample.html-iPAR6SxB.js"),__vite__mapDeps([96,1])),meta:{d:1634871035e3,g:["Consul","Consul Template"],e:` +참고 : https://learn.hashicorp.com/tutorials/consul/consul-template
+템플릿 파일 변환 하기
+템플릿 파일 작성
++
+- 대상 kv : apache/version
+`,r:{minutes:.36,words:109},y:"a",t:"KV Sample"}}],["/04-HashiCorp/04-Consul/05-Template_Sample/nginx.html",{loader:()=>u(()=>import("./nginx.html-B52ZmRRf.js"),__vite__mapDeps([97,1])),meta:{d:1634871035e3,g:["Consul","Consul Template","NGINX"],e:` +# apache_install.sh.ctmpl +#!/bin/bash +sudo apt-get remove -y apache2 +sudo apt-get install -y apache2={{ key "/apache/version" }} +
참고 : https://learn.hashicorp.com/tutorials/consul/load-balancing-nginx
+템플릿 파일 변환 하기
+템플릿 파일 작성
++
+- 대상 서비스 : nginx-backend
+`,r:{minutes:.36,words:108},y:"a",t:"NGINX Sample"}}],["/04-HashiCorp/05-Boundary/01-Install/OnConsulNomad.html",{loader:()=>u(()=>import("./OnConsulNomad.html-PufX9nh_.js"),__vite__mapDeps([98,1])),meta:{d:1634219407e3,g:["Boundary","Install"],e:` +# nginx.conf.ctmpl +upstream backend { + {{- range service "nginx-backend" }} + server {{.Address}}:{{.Port}} max_fails=3 fail_timeout=60 weight=1; + {{else}}server 127.0.0.1:65535; # force a 502 + {{- end}} +} + +server { + listen 80 default_server; + + location /stub_status { + stub_status; + } + + location / { + proxy_pass http://backend; + } +} +
1. Nomad namespace create
+nomad namespace apply -description "Boundary" boundary +
2. Postgresql setup
`,r:{minutes:2.29,words:688},y:"a",t:"Boundary Install on Consul-Nomad"}}],["/04-HashiCorp/05-Boundary/01-Install/OnNomad-devmode.html",{loader:()=>u(()=>import("./OnNomad-devmode.html-DozOQZMV.js"),__vite__mapDeps([99,1])),meta:{d:1653031029e3,g:["Boundary","Install"],e:` +1. Job sample
+`,r:{minutes:.44,words:132},y:"a",t:"Boundary Run Dev Mode on Nomad Job"}}],["/04-HashiCorp/05-Boundary/02-Config/BoundaryTerraformSample01.html",{loader:()=>u(()=>import("./BoundaryTerraformSample01.html-68Tij93V.js"),__vite__mapDeps([100,1])),meta:{d:1634219407e3,g:["Boundary","Terraform","Config"],e:` +locals { + version = "0.8.1" + private_ip = "192.168.0.27" + public_ip = "11.129.13.30" +} + +job "boundary-dev" { + type = "service" + datacenters = ["home"] + namespace = "boundary" + + constraint { + attribute = "${attr.os.name}" + value = "raspbian" + } + + group "dev" { + count = 1 + + ephemeral_disk { sticky = true } + + network { + mode = "host" + port "api" { + static = 9200 + to = 9200 + } + port "cluster" { + static = 9201 + to = 9201 + } + port "worker" { + static = 9202 + to = 9202 + } + } + + task "dev" { + driver = "raw_exec" + + env { + BOUNDARY_DEV_CONTROLLER_API_LISTEN_ADDRESS = local.private_ip + BOUNDARY_DEV_CONTROLLER_CLUSTER_LISTEN_ADDRESS = "0.0.0.0" + BOUNDARY_DEV_WORKER_PUBLIC_ADDRESS = local.public_ip + BOUNDARY_DEV_WORKER_PROXY_LISTEN_ADDRESS = local.private_ip + BOUNDARY_DEV_PASSWORD = "password" + } + + // artifact { + // source = "https://releases.hashicorp.com/boundary/\${local.version}/boundary_\${local.version}_linux_arm.zip" + // } + + config { + command = "boundary" + args = ["dev"] + } + + resources { + cpu = 500 + memory = 500 + } + + service { + name = "boundary" + tags = ["cluster"] + + port = "cluster" + + check { + type = "tcp" + interval = "10s" + timeout = "2s" + port = "api" + } + } + } + } +} +
+
`,r:{minutes:1.72,words:516},y:"a",t:"Configure Boudary using Terraform"}}],["/04-HashiCorp/06-Vault/01-Information/kmip-faq.html",{loader:()=>u(()=>import("./kmip-faq.html-C7ANvaZR.js"),__vite__mapDeps([101,1])),meta:{a:"hashicat(MZC), chadness12(MZC)",d:1707979472e3,g:["vault","kmip"],e:` + +- Terraform provider : https://registry.terraform.io/providers/hashicorp/boundary/latest/docs
+- learn site : https://learn.hashicorp.com/tutorials/boundary/getting-started-config
+Client 인증서의 유효 기간
+기본 설정시 1,209,600초(2주)의 유효 기간을 갖게 되며, 설정에 따라 긴 유효시간의 적용이 가능합니다. (옵션 :
`,r:{minutes:1.6,words:480},y:"a",t:"Vault KMIP FAQ"}}],["/04-HashiCorp/06-Vault/01-Information/port-info.html",{loader:()=>u(()=>import("./port-info.html-pJreTwb3.js"),__vite__mapDeps([102,1])),meta:{d:1640239298e3,g:["vault","port","requirement"],e:` +deault_tls_client_ttl
)
+설정은 상기 도식화한 절차 중 "2. kmip 기본 config" 단계에 적용 가능하며. 이는KMIP 적용 흐름도
의 "4. kmip scope, role 정의" 단계에서 override 할 수 있습니다.++https://learn.hashicorp.com/tutorials/vault/reference-architecture#network-connectivity
+Vault 포트
+TCP
++
`,r:{minutes:.51,words:154},y:"a",t:"Vault Listen Address & Port"}}],["/04-HashiCorp/06-Vault/01-Information/vault-audit.html",{loader:()=>u(()=>import("./vault-audit.html-CDGLjQ2_.js"),__vite__mapDeps([103,1])),meta:{d:1641009317e3,g:["vault","audit"],e:` +- Url : https://www.vaultproject.io/docs/configuration/listener/tcp#tcp-listener-parameters +
++
+- address default :
+127.0.0.1:8200
- cluster_address default :
+127.0.0.1:8201
Vault Audit은
+-path
를 달리하여 여러 Audit 메커니즘을 중복해서 구성 가능File
+`,r:{minutes:.22,words:65},y:"a",t:"Vault Audit"}}],["/04-HashiCorp/06-Vault/01-Information/vault-dev-mode-option.html",{loader:()=>u(()=>import("./vault-dev-mode-option.html-COroIWK7.js"),__vite__mapDeps([104,1])),meta:{d:1676188911e3,g:["vault","optinos"],e:` +$ vault audit enable file file_path=/var/log/vault/vault_audit.log +$ vault audit enable -path=file2 file file_path=/var/log/vault/vault_audit2.log +
볼트 개발 모드 서버를 시작하는 기초적인 커맨드와 실행 후 안내 메시지는 다음과 같다.
+`,r:{minutes:1.49,words:447},y:"a",t:"Vault Development Mode Options"}}],["/04-HashiCorp/06-Vault/01-Information/vault-server-configuration-info.html",{loader:()=>u(()=>import("./vault-server-configuration-info.html-DINKbJUe.js"),__vite__mapDeps([105,1])),meta:{d:1676186503e3,g:["vault","configuration"],e:` +$ vault server -dev + +==> Vault server configuration: + + Api Address: http://127.0.0.1:8200 + Cgo: disabled + Cluster Address: https://127.0.0.1:8201 + Environment Variables: HOME, ITERM_PROFILE, ... + Go Version: go1.19.4 + Listener 1: tcp (addr: "127.0.0.1:8200", cluster address: "127.0.0.1:8201", max_request_duration: "1m30s", max_request_size: "33554432", tls: "disabled") + Log Level: info + Mlock: supported: false, enabled: false + Recovery Mode: false + Storage: inmem + Version: Vault v1.12.3, built 2023-02-02T09:07:27Z + Version Sha: 209b3dd99fe8ca320340d08c70cff5f620261f9b + +==> Vault server started! Log data will stream in below: + +... +
볼트 서버를 시작하는 기초적인 커맨드와 실행 후 안내 메시지는 다음과 같다.
+`,r:{minutes:.64,words:193},y:"a",t:"Vault Server Configuration - Info"}}],["/04-HashiCorp/06-Vault/01-Information/vault-sizing.html",{loader:()=>u(()=>import("./vault-sizing.html-BIukP5am.js"),__vite__mapDeps([106,1])),meta:{d:1641009317e3,g:["vault","sizing"],e:` +$ vault server -dev + +==> Vault server configuration: + + Api Address: http://127.0.0.1:8200 + Cgo: disabled + Cluster Address: https://127.0.0.1:8201 + Environment Variables: HOME, ITERM_PROFILE, ... + Go Version: go1.19.4 + Listener 1: tcp (addr: "127.0.0.1:8200", cluster address: "127.0.0.1:8201", max_request_duration: "1m30s", max_request_size: "33554432", tls: "disabled") + Log Level: info + Mlock: supported: false, enabled: false + Recovery Mode: false + Storage: inmem + Version: Vault v1.12.3, built 2023-02-02T09:07:27Z + Version Sha: 209b3dd99fe8ca320340d08c70cff5f620261f9b + +==> Vault server started! Log data will stream in below: + +... +
+`,r:{minutes:.65,words:196},y:"a",t:"Vault Sizing"}}],["/04-HashiCorp/06-Vault/01-Information/vault-token.html",{loader:()=>u(()=>import("./vault-token.html-DztbiN7r.js"),__vite__mapDeps([107,1])),meta:{d:1677917082e3,g:["vault","token"],e:` +https://learn.hashicorp.com/tutorials/vault/reference-architecture#deployment-system-requirements
+Vault의 Backend-Storage 사용 여부에 따라 구성에 차이가 발생
++ ++`,r:{minutes:3.45,words:1034},y:"a",t:"Token의 이해"}}],["/04-HashiCorp/06-Vault/02-Secret_Engine/keymgmt.html",{loader:()=>u(()=>import("./keymgmt.html-10SIymGL.js"),__vite__mapDeps([108,1])),meta:{d:1691365293e3,g:["vault","Vault Enterprise","keymgmt"],e:` +++Key Management Secret Engine을 활성화 하기 위해서는
+ADP
수준의 라이선스가 필요하다.Key Management 시크릿 엔진은 KMS(Key Management Service)를 공급하는 대상의 암호화 키의 배포 및 수명 주기 관리를 위한 워크플로를 제공한다. KMS 공급자 고유의 암호화 기능을 기존처럼 사용하면서도, 볼트에서 키를 중앙 집중식으로 제어할 수 있다.
+볼트는 KMS의 구성에 사용되는 Key Meterial 원본을 생성하여 보유한다. 관리가능한 KMS에 대해 키 수명주기를 설정 및 관리하면 Key Meterial의 복사본이 대상에 배포된다. 이 방식으로 볼트는 KMS 서비스의 전체 수명 주기 관리 및 키 복구 수단을 제공한다. 지원되는 KMS는 다음과 같다.
`,r:{minutes:13.59,words:4078},y:"a",t:"Key Management"}}],["/04-HashiCorp/06-Vault/02-Secret_Engine/kmip-mongo.html",{loader:()=>u(()=>import("./kmip-mongo.html-XXGOUK4V.js"),__vite__mapDeps([109,1])),meta:{d:1641009317e3,g:["vault","Vault Enterprise","KMIP","MongoDB"],e:` +++Enterprise 기능
+Vault - dev mode run (Option)
+`,r:{minutes:.85,words:256},y:"a",t:"KMIP - MongoDB"}}],["/04-HashiCorp/06-Vault/02-Secret_Engine/pki-nginx.html",{loader:()=>u(()=>import("./pki-nginx.html-CUq8HqPC.js"),__vite__mapDeps([110,1])),meta:{d:1641009317e3,g:["vault","PKI"],e:` +VAULT_UI=true vault server -dev-root-token-id=root -dev -log-level=trace + +export VAULT_ADDR="http://127.0.0.1:8200" +echo "export VAULT_ADDR=$VAULT_ADDR" >> /root/.bashrc +vault status +vault login root +
+ ++Vault 구성
+환경 변수
+`,r:{minutes:3.2,words:960},y:"a",t:"PKI - nginx 샘플"}}],["/04-HashiCorp/06-Vault/02-Secret_Engine/ssh-otp-debian.html",{loader:()=>u(()=>import("./ssh-otp-debian.html-DLDjZJ9W.js"),__vite__mapDeps([111,1])),meta:{d:1641009317e3,g:["vault","SSH","OTP","Debian","Ubuntu"],e:` +export VAULT_SKIP_VERIFY=True +export VAULT_ADDR='http://172.28.128.11:8200' +export VAULT_TOKEN=s.8YXFI825TZxnwLtYHsLc9Fnb +
+ ++Vault설정
+시크릿 엔진 활성화
+`,r:{minutes:1.11,words:333},y:"a",t:"SSH OTP - Debian 계열"}}],["/04-HashiCorp/06-Vault/02-Secret_Engine/ssh-otp-redhat.html",{loader:()=>u(()=>import("./ssh-otp-redhat.html-DwB1_tZt.js"),__vite__mapDeps([112,1])),meta:{d:1641009317e3,g:["vault","SSH","OTP","Rocky","RHEL","CentOS"],e:` +$ vault secrets enable -path ssh ssh +
+ ++Vault설정
+시크릿 엔진 활성화
+`,r:{minutes:1.05,words:315},y:"a",t:"SSH OTP - RedHat 계열"}}],["/04-HashiCorp/06-Vault/02-Secret_Engine/ssh-signed-certificates.html",{loader:()=>u(()=>import("./ssh-signed-certificates.html-BukdSfUL.js"),__vite__mapDeps([113,1])),meta:{d:1641009317e3,g:["vault","SSH"],e:` +$ vault secrets enable -path ssh ssh +
Vault설정
+시크릿 엔진 활성화
+`,r:{minutes:.74,words:222},y:"a",t:"SSH - Signed Certificates"}}],["/04-HashiCorp/06-Vault/02-Secret_Engine/transform-fpe.html",{loader:()=>u(()=>import("./transform-fpe.html-QAq4yL3V.js"),__vite__mapDeps([114,1])),meta:{d:1645244902e3,g:["vault","transform","fpe"],e:` +$ vault secrets enable -path=ssh-client-signer ssh +
Transform secrets 엔진은 제공된 입력 값에 대해 안전한 데이터 변환 및 토큰화를 처리합니다. 변환 방법은 FF3-1 을 통한 형태 보존 암호화(FPE) 와 같은 NIST 검증된 암호화 표준을 포함 할 수 있지만 마스킹과 같은 다른 수단을 통한 데이터의 익명 변환일 수도 있습니다.
++
`,r:{minutes:.87,words:260},y:"a",t:"Transform FPE (Ent)"}}],["/04-HashiCorp/06-Vault/02-Secret_Engine/transit-import.html",{loader:()=>u(()=>import("./transit-import.html-C9MCha3F.js"),__vite__mapDeps([115,1])),meta:{d:1695292702e3,g:["vault","transit"],e:` +- Doc : https://www.vaultproject.io/docs/secrets/transform
+- Learn : https://learn.hashicorp.com/tutorials/vault/transform
+키 가져오기(Import) 기능은 HSM, 사용자 정의 키, 기타 외부 시스템에서 기존 키를 가져와야 하는 경우를 지원한다. 공개키(Public Key)만을 가져올 수도 있다.
++`,r:{minutes:4.53,words:1360},y:"a",t:"Transit (Import)"}}],["/04-HashiCorp/06-Vault/02-Secret_Engine/transit.html",{loader:()=>u(()=>import("./transit.html-D29kRUEL.js"),__vite__mapDeps([116,1])),meta:{d:1641009317e3,g:["vault","transit"],e:` + +links
+ +Vault구성 (Option)
+시크릿 엔진 활성화
+`,r:{minutes:1.44,words:433},y:"a",t:"Transit"}}],["/04-HashiCorp/06-Vault/03-Auth_Method/aws-auth.html",{loader:()=>u(()=>import("./aws-auth.html-CZDrbpdq.js"),__vite__mapDeps([117,1])),meta:{d:1688129915e3,g:["vault auth","AWS"],e:` +export VAULT_SKIP_VERIFY=True +export VAULT_ADDR='http://172.28.128.21:8200' +export VAULT_TOKEN=<mytoken> +
+`,r:{minutes:4.49,words:1346},y:"a",t:"AWS Auth Method"}}],["/04-HashiCorp/06-Vault/03-Auth_Method/mfa-login.html",{loader:()=>u(()=>import("./mfa-login.html-BW3fIyzd.js"),__vite__mapDeps([118,1])),meta:{d:1651826418e3,g:["vault auth","MFA"],e:` +https://developer.hashicorp.com/vault/docs/auth/aws
+ + ++`,r:{minutes:.94,words:282},y:"a",t:"MFA Login with Vault TOTP"}}],["/04-HashiCorp/06-Vault/03-Auth_Method/super-user-create.html",{loader:()=>u(()=>import("./super-user-create.html-BgsspQ9P.js"),__vite__mapDeps([119,1])),meta:{d:1641009317e3,g:["vault auth"],e:` +HashiCorp Learn - Login MFA : https://learn.hashicorp.com/tutorials/vault/multi-factor-authentication
+
+Configure TOTP MFA Method : https://www.vaultproject.io/api-docs/secret/identity/mfa/totp
+Vault Login MFA Overview : https://www.vaultproject.io/docs/auth/login-mfa
+1.10.3+ recommend : https://discuss.hashicorp.com/t/vault-1-10-3-released/39394++주의
+해당 방법은 username/password 방식의 Admin권한의 사용자를 생성하나,
+
+보안상 실 구성에는 권장하지 않습니다.+
+- userpass 활성화
+`,r:{minutes:.64,words:192},y:"a",t:"Vault SuperUser 생성"}}],["/04-HashiCorp/06-Vault/03-Auth_Method/token_role.html",{loader:()=>u(()=>import("./token_role.html-B8tnjLEI.js"),__vite__mapDeps([120,1])),meta:{d:1651059687e3,g:["vault auth"],e:` +vault auth enable userpass +
별도 Auth Method를 사용하지 않고 Token으로만 사용하는 경우 Token에 대한 role을 생성하여 해당 role의 정의된 설정에 종속된 Token을 생성할 수 있음
++
+- Entity가 발생하므로 Vault Client Count 절약 가능
+- 일관된 Token 생성 가능
+- Token에 대한 별도 Tune(TTL 조정 등) 가능
+절차
++
`,r:{minutes:1.18,words:353},y:"a",t:"Token Role"}}],["/04-HashiCorp/06-Vault/03-Auth_Method/vault-kv-v2-ui-policy.html",{loader:()=>u(()=>import("./vault-kv-v2-ui-policy.html-D5LINTkA.js"),__vite__mapDeps([121,1])),meta:{d:1641009317e3,g:["vault","kv","policy"],e:` +- +
+UI > Access > Entities > [create entity] :
+100y-entity
- +
+entity에서 aliases 생성 :
+100y-alias
- +
+role 생성 (payload.json)
+{ + "allowed_policies": [ + "my-policy" + ], + "name": "100y", + "orphan": false, + "bound_cidrs": ["127.0.0.1/32", "128.252.0.0/16"], + "renewable": true, + "allowed_entity_aliases": ["100y-alias"] +} +
- +
+role 적용
+curl -H "X-Vault-Token: hvs.QKRiVmCedA06dCSc2TptmSk1" -X POST --data @payload.json http://127.0.0.1:8200/v1/auth/token/roles/100y +
- +
+role에 대한 사용자 정의 tune 적용(옵션)
+vault auth tune -max-lease-ttl=876000h token/role/100y +vault auth tune -default-lease-ttl=876000h token/role/100y +
- +
+tune 적용된 role 확인
+$ vault read auth/token/roles/100y + +Key Value +--- ----- +allowed_entity_aliases [100y-alias] +allowed_policies [default] +allowed_policies_glob [] +bound_cidrs [127.0.0.1 128.252.0.0/16] +disallowed_policies [] +disallowed_policies_glob [] +explicit_max_ttl 0s +name 100y +orphan false +path_suffix n/a +period 0s +renewable true +token_bound_cidrs [127.0.0.1 128.252.0.0/16] +token_explicit_max_ttl 0s +token_no_default_policy false +token_period 0s +token_type default-service +
- +
+token 생성
+$ vault token create -entity-alias=100y-alias -role=100y +Key Value +--- ----- +token hvs.CAESIIveQyE34VOowkCXj4InopxsQHWXu2iW00UQDDCTb-pIGh4KHGh2cy5UZGJ4MjJic1RjY1BlVGRWVHhzNFgwWW4 +token_accessor Cx6qjyUGwqPmqoPNe9tmkCiN +token_duration 876000h +token_renewable true +token_policies ["default"] +identity_policies ["default"] +policies ["default"] +
- +
+token이 role의 구성이 반영되었는지 확인
+$ vault token lookup hvs.CAESIIveQyE34VOowkCXj4InopxsQHWXu2iW00UQDDCTb-pIGh4KHGh2cy5UZGJ4MjJic1RjY1BlVGRWVHhzNFgwWW4 + +Key Value +--- ----- +accessor Cx6qjyUGwqPmqoPNe9tmkCiN +bound_cidrs [127.0.0.1 128.252.0.0/16] +creation_time 1651059486 +creation_ttl 876000h +display_name token +entity_id 53fc4716-fc0d-db34-14b8-ab4258f89fb1 +expire_time 2122-04-03T20:38:06.73198+09:00 +explicit_max_ttl 0s +external_namespace_policies map[] +id hvs.CAESIIveQyE34VOowkCXj4InopxsQHWXu2iW00UQDDCTb-pIGh4KHGh2cy5UZGJ4MjJic1RjY1BlVGRWVHhzNFgwWW4 +identity_policies [default] +issue_time 2022-04-27T20:38:06.731984+09:00 +meta <nil> +num_uses 0 +orphan false +path auth/token/create/100y +policies [default] +renewable true +role 100y +ttl 875999h59m3s +type service +
++사용자별 UI 접근에 대한 설정을 Kv-v2를 예로 확인
+Policy 구성
+UI 접근을 위해서는
+metadata
에 대한 권한 추가가 필요함`,r:{minutes:.76,words:227},y:"a",t:"kv-v2 UI Policy"}}],["/04-HashiCorp/06-Vault/04-UseCase/argocd-vault-plugin.html",{loader:()=>u(()=>import("./argocd-vault-plugin.html-Bs4TVigs.js"),__vite__mapDeps([122,1])),meta:{d:1686536963e3,g:["vault","argocd","gitops","devsescops","pipeline","github","gitlab","secret","kubernetes","k8s","eks"],e:` +$ vault policy write ui-kv-policy - << EOF + +path "kv-v2/data/path/" { + capabilities = ["create", "update", "read", "delete", "list"] +} +path "kv-v2/delete/path/" { + capabilities = ["update"] +} +path "kv-v2/metadata/path/" { + capabilities = ["list", "read", "delete"] +} +path "kv-v2/destroy/path/" { + capabilities = ["update"] +} + +path "kv-v2/data/path/userid/*" { + capabilities = ["create", "update", "read", "delete", "list"] +} +path "kv-v2/delete/path/userid/*" { + capabilities = ["update"] +} +path "kv-v2/metadata/path/userid/*" { + capabilities = ["list", "read", "delete"] +} +path "kv-v2/destroy/path/userid/*" { + capabilities = ["update"] +} + +# Additional access for UI +path "kv-v2/metadata" { + capabilities = ["list"] +} +EOF + +##### or ##### + +vault policy write ui-kv-policy - << EOF + +path "kv-v2/data/path/userid" { + capabilities = ["create", "update", "read", "delete", "list"] +} +path "kv-v2/delete/path/userid" { + capabilities = ["update"] +} +path "kv-v2/metadata/path/userid" { + capabilities = ["list", "read", "delete"] +} +path "kv-v2/destroy/path/userid" { + capabilities = ["update"] +} + +# Additional access for UI +path "kv-v2/metadata/*" { + capabilities = ["list"] +} +EOF + +
++참고 : 본 글은 AEWS 스터디 7주차 내용중 일부로 작성된 내용입니다.
+1. ArgoCD
+ +1) 개요 및 소개
+Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes.
`,r:{minutes:7.09,words:2127},y:"a",t:"ArgoCD Vault Plugin"}}],["/04-HashiCorp/06-Vault/04-UseCase/jenkins-pipeilne-vault-approle.html",{loader:()=>u(()=>import("./jenkins-pipeilne-vault-approle.html-BSKyMc3X.js"),__vite__mapDeps([123,1])),meta:{d:1656429396e3,g:["vault","jenkins","approle"],e:` +Vault의 AppRole 인증 방식은 Vault Token을 얻기위한 단기 자격증명을 사용하는 장점이 있지만 자동화된 환경에 어울리는(반대로 사람에게 불편한)방식으로 Vault를 이용하는 애플리케이션/스크립트의 배포 파이프라인을 구성하는 방식을 추천합니다.
+`,r:{minutes:5.45,words:1635},y:"a",t:"Jenkins Pipeline Vault Approle (with Nomad)"}}],["/04-HashiCorp/06-Vault/04-UseCase/jenkins-with-vault-otp.html",{loader:()=>u(()=>import("./jenkins-with-vault-otp.html-CgbhmfaN.js"),__vite__mapDeps([124,1])),meta:{d:1645000713e3,g:["vault","jenkins","otp","token"],e:` +TEST ENV
+$ sw_vers +ProductName: macOS +ProductVersion: 12.4 + +$ brew --version +Homebrew 3.5.2 + +$ git version +git version 2.27.0 + +$ java -version +openjdk version "11.0.14.1" 2022-02-08 + +$ gradle --version +Welcome to Gradle 7.4.2! + +$ docker version +Client: + Version: 20.10.9 + +Server: + Engine: + Version: 20.10.14 + +$ vault version +Vault v1.11.0 + +$ nomad version +Nomad v1.3.1 + +$ curl --version +curl 7.79.1 (x86_64-apple-darwin21.0) +
jenkins와 vault otp를 연동하여 pipe line에서 ssh/scp test
+otp 설정은 docmoa의 ssh-otp 참고
++
+- OTP 설정 링크
+vault token 설정
+`,r:{minutes:.66,words:198},y:"a",t:"jenkins with vault otp"}}],["/04-HashiCorp/06-Vault/04-UseCase/jenkins-with-vault.html",{loader:()=>u(()=>import("./jenkins-with-vault.html-Bquo_ttW.js"),__vite__mapDeps([125,1])),meta:{d:1643344857e3,g:["vault","jenkins","screct","kv"],e:` +# ssh 권한을 사용 할 policy 생성 +$ tee ssh-policy.hcl <<EOF +# To list SSH secrets paths +path "ssh/*" { + capabilities = [ "list" ] +} +# To use the configured SSH secrets engine otp_key_role role +path "ssh/creds/otp_key_role" { + capabilities = ["create", "read", "update"] +} +EOF + +#ssh(otp) 정책 생성 +$ vault policy write ssh ssh-policy.hcl + +#rest api에서 사용 할 token 생성 +$ vault token create -policy=ssh +
jenkins와 vault를 연동하여 pipe line에서 kv 사용하기
+
+이 예제는 진짜 kv까지만 테스트함`,r:{minutes:.86,words:259},y:"a",t:"jenkins with vault"}}],["/04-HashiCorp/06-Vault/04-UseCase/mtls.html",{loader:()=>u(()=>import("./mtls.html-yuwnSBw9.js"),__vite__mapDeps([126,1])),meta:{d:1679214075e3,g:["vault","pki","mTLS"],e:` +# approle 엔진 생성 +$ vault auth enable approle +# kv2 enable +$ vault secrets enable kv-v2 +# kv enable +$ vault secrets enable -path=kv kv + +# jenkins 정책으로 될 파일 생성 v1, v2 +$ tee jenkins-policy.hcl <<EOF +path "kv/secret/data/jenkins/*" { + capabilities = [ "read" ] +} +path "kv-v2/data/jenkins/*" { + capabilities = [ "read" ] +} +EOF + +#jenkins 정책 생성 +vault policy write jenkins jenkins-policy.hcl + +#approle 생성 및 정책 jenkins에 연결 +vault write auth/approle/role/jenkins token_policies="jenkins" \\ +token_ttl=1h token_max_ttl=4h + +#Role id, secret id 가져오기 + +vault read auth/approle/role/jenkins/role-id +vault write -f auth/approle/role/jenkins/secret-id + + +vault secrets enable -path=kv kv +$ tee gitlab.json <<EOF +{ + "gitlabIP": "172.21.2.52", + "api-key": "RjLAbbWsSAzXoyBvo2qL" +} +EOF + +tee gitlab-v2.json <<EOF +{ + "gitlabIP": "172.21.2.52", + "api-key": "RjLAbbWsSAzXoyBvo2qL", + "version": "v2" +} +EOF + +vault kv put kv/secret/data/jenkins/gitlab @gitlab.json +vault kv put kv-v2/jenkins/gitlab @gitlab-v2.json +
++Demo App Github : https://github.com/Great-Stone/vault-mtls-demo
+1. mTLS 설명
+1.1 SSL과 TLS
+SSL(Secure Sokets Layer, 보안 소캣 계층)는 클라이언트와 서버 사이에 전송된 데이터를 암호화 하고 인터넷 연결에 보안을 유지하는 표준 기술이다. 악의적 외부인이 클라이언트와 서버 사이에 전송되는 정보를 확인 및 탈취하는 것을 방지한다.
`,r:{minutes:8.66,words:2599},y:"a",t:"Vault PKI - mTLS demo"}}],["/04-HashiCorp/06-Vault/04-UseCase/nomad-integration.html",{loader:()=>u(()=>import("./nomad-integration.html-Dc0XFJO_.js"),__vite__mapDeps([127,1])),meta:{d:16536985e5,g:["nomad","vault","aws","db"],e:` ++`,r:{minutes:4.24,words:1271},y:"a",t:"Vault & Nomad Integration Test"}}],["/04-HashiCorp/06-Vault/04-UseCase/sentinel-check-identity-cidr.html",{loader:()=>u(()=>import("./sentinel-check-identity-cidr.html-695nF6hn.js"),__vite__mapDeps([128,1])),meta:{d:1668567247e3,g:["vault","sentinel","cidr","enterprise"],e:` +Dev Mode 를 활용한 테스트
++
+- +
+목적 : Spring boot 기반 애플리케이션에서 Nomad 를 이용하여 Vault의 dynamic secret 을 최소한의 코드변경으로 사용할 수 있는 워크 플로우 구성
+- +
+코드 기반 인경우의 예제 : https://dev.to/aws-builders/aws-sts-with-spring-cloud-vault-1e5g
+- +
+Vault-Nomad Integration : https://www.nomadproject.io/docs/integrations/vault-integration
+- +
+Version (Download)
++
+- Nomad v1.3.1 (2b054e38e91af964d1235faa98c286ca3f527e56)
+- Vault v1.10.3 (af866591ee60485f05d6e32dd63dde93df686dfb)
+- +
+Kubernetes 환경인 경우 Vault CSI Provider를 통해 비슷한 구성 가능 : https://www.vaultproject.io/docs/platform/k8s/csi
+++Enterprise 기능
+Token Role에
+bound_cidr
을 적용하거나 여타 인증(AppRole, Userpass 등)에 허용하는 cidr을 적용하는 경우 다시 Token을 발급하거나 인증받지 않는한은 cidr을 기반으로한 차단을 동적으로 적용할 수 없다.이경우 Sentinel을 사용하여 동적인 정책을 적용할 수 있다. Sentinel은 ACL방식의 기존
`,r:{minutes:1.32,words:395},y:"a",t:"Sentinel - (Identity & CIDR)"}}],["/04-HashiCorp/06-Vault/04-UseCase/spring-boot.html",{loader:()=>u(()=>import("./spring-boot.html-2E8NVdcM.js"),__vite__mapDeps([129,1])),meta:{d:1681084498e3,g:["vault","java","spring"],e:` +Policy
와는 달리 Path가 아닌 다른 검증 조건을 추가할 수 있다.++Example Source : https://github.com/Great-Stone/vault_springboot_example
+볼트는 애플리케이션(앱)의 구성관리, 특히 사용자 ID, 패스워드, Token, 인증서, 엔드포인트, AWS 자격증명 등과 같은 민감한 정보를 안전하게 저장하는 중앙 집중식 인프라를 제공한다. 서비스의 성장과 더불어, 이를 구성하는 앱은 확장과 분리 요구 사항이 발생하면 구성 관리가 어려워 진다. 특히, 시크릿 정보가 포함되는 구성 관리는 수동으로 관리하는 경우 로컬 환경을 포함한 여러 시스템에 노출되는 위험성을 갖고, 환경마다 다른 시크릿을 관리하기위한 유지 관리의 노력과 비용이 증가한다. 볼트에서 이야기하는 앱과 관련한 "시크릿 스프롤(퍼짐)" 현상은 다음과 같다.
`,r:{minutes:5.07,words:1520},y:"a",t:"Vault로 Spring Boot 구성관리"}}],["/04-HashiCorp/06-Vault/04-UseCase/terraform-with-aws-secret-engine.html",{loader:()=>u(()=>import("./terraform-with-aws-secret-engine.html-BWFLLgFQ.js"),__vite__mapDeps([130,1])),meta:{a:"powhapki",d:1650373852e3,g:["terraform","vault","aws"],e:` ++`,r:{minutes:1.18,words:353},y:"a",t:"Terraform 코드 상에서 Vault 연동하기"}}],["/04-HashiCorp/06-Vault/04-UseCase/transit-stress-test.html",{loader:()=>u(()=>import("./transit-stress-test.html-wiHaB4DE.js"),__vite__mapDeps([131,1])),meta:{d:1647319815e3,g:["vault","performance","transit"],e:` +팁
+Inject Secrets into Terraform Using the Vault Provider
+ + ++`,r:{minutes:.51,words:153},y:"a",t:"Vault Stress Test"}}],["/04-HashiCorp/06-Vault/04-UseCase/vault-k8s-integration-three-methods.html",{loader:()=>u(()=>import("./vault-k8s-integration-three-methods.html-LiLO8xBQ.js"),__vite__mapDeps([132,1])),meta:{d:1682257604e3,g:["vault","kubernetes","secret","VSO"],e:` +wrk github : https://github.com/wg/wrk
+
+transit : https://www.vaultproject.io/docs/secrets/transit+
+- Sidecar Agent Injector
+- CSI provider
+- Vault Secrets Operator
++`,r:{minutes:3.22,words:967},y:"a",t:"Kubernetes Vault 통합방안 3가지 비교"}}],["/04-HashiCorp/06-Vault/04-UseCase/vault-k8s-manually-using-the-sidecar.html",{loader:()=>u(()=>import("./vault-k8s-manually-using-the-sidecar.html-CCPhH6l3.js"),__vite__mapDeps([133,1])),meta:{d:1701658962e3,g:["vault","kubernetes"],e:` +개요
+본 글에서는 HashiCorp Vault 및 Kubernetes 통합을 위해 HashiCorp가 지원하는 세 가지 방법을 자세히 비교한다:
++
+- 볼트 사이드카 에이전트 인젝터(Sidecar Agent Injector)
+- 볼트 컨테이너 스토리지 인터페이스 공급자(Container Storage Interface (CSI) provider)
+- 볼트 시크릿 오퍼레이터(Secrets Operator)
+각 방법에 대한 실용적인 지침(guidance)을 제공하여 사용 사례에 가장 적합한 방법을 이해하고 선택할 수 있도록 안내한다.
+Kubernetes(K8s)환경에서 외부 Vault(External Vault Server)와 연계하는 경우 일반적으로
`,r:{minutes:2.59,words:777},y:"a",t:"Kubernetes에 Vault Agent(Sidecar) 수동 구성"}}],["/04-HashiCorp/06-Vault/04-UseCase/vault-k8s-usecase-csi-injection.html",{loader:()=>u(()=>import("./vault-k8s-usecase-csi-injection.html-zpyEzJMk.js"),__vite__mapDeps([134,1])),meta:{d:1678338483e3,g:["vault","kubernetes"],e:` +kubernetes
인증방식을 활용하여 Vault와 K8s 간 플랫폼 수준에서의 인증을 처리하나, K8s로의 Cluster API에 대한 inbound가 막혀있는 경우 이같은 방식은 사용할 수 없다. 따라서helm
,vso
같은 방식의 사용이 불가능하므로 Vault Agent를 Sidecar로 함께 배포하는 경우 수동으로 구성해주어야 한다.Vault에 저장된 시크릿 또는 발행되는(Dynamic) 시크릿을 획득하기 위해서는, 시크릿을 요청하는 클라이언트(사람/앱/장비)가 다음의 과정을 수행해야 합니다.
++
+- 클라이언트가 Vault 토큰을 획득하기 위한 인증 절차
+- 획득한 Vault 토큰의 수명주기 관리 (갱신과 재요청)
+- Vault의 특정 시크릿 경로를 저장하고 해당 시크릿 요청
+- 동적(Dynamic) 시크릿인 경우 해당 임대(Lease)정보 확인 및 갱신과 재요청
+Vault는 위의 과정을 클라이언트 대신 플랫폼 수준에서 대행할 수 있는 방안을 제공하고 있습니다. 여기서는 Kubernetes 상에서의 Vault와의 통합 구성을 활용하여 위 과정을 대체하고 Kubernetes 플랫폼 자체(Kuberetes Native)의 기능을 사용하듯 Vault의 시크릿을 사용하게 만드는 방식에 대해 설명합니다.
`,r:{minutes:12.85,words:3855},y:"a",t:"How to integrate Vault with K8s (CSI & Injection & VSO)"}}],["/04-HashiCorp/06-Vault/04-UseCase/windows-password-rotation.html",{loader:()=>u(()=>import("./windows-password-rotation.html-BsqYRvmj.js"),__vite__mapDeps([135,1])),meta:{d:1641009317e3,g:["vault","windows","nomad","password"],e:` ++ ++Kv 추가
+`,r:{minutes:2.12,words:636},y:"a",t:"Windows Password rotation"}}],["/04-HashiCorp/06-Vault/05-TroubleShooting/400-error.html",{loader:()=>u(()=>import("./400-error.html-De4cfBHo.js"),__vite__mapDeps([136,1])),meta:{d:1647319815e3,g:["vault","error","400"],e:` +$ vault secrets enable -version=2 -path=systemcreds/ kv +
++Vault HTTP Status Codes : https://www.vaultproject.io/api#http-status-codes
+Vault에 API 요청시 400에러가 발생하는 경우 Vault로 전달된 데이터 형태가 올바른지 확인이 필요하다.
++
`,r:{minutes:.46,words:139},y:"a",t:"Vault 400 Error"}}],["/04-HashiCorp/06-Vault/05-TroubleShooting/vault-sizing.html",{loader:()=>u(()=>import("./vault-sizing.html-CuSmCHes.js"),__vite__mapDeps([137,1])),meta:{d:1641009317e3,g:["vault","MiriaDB"],e:` +- +
400
: Invalid request, missing or invalid data.+
+- +
+현상 : $vault read mysql/creds/my-role 입력시 오류
+- +
+오류 내용 :
+`,r:{minutes:.39,words:116},y:"a",t:"Vault MariaDB5.5 Dynamic Secret"}}],["/04-HashiCorp/06-Vault/06-Config/tls-config.html",{loader:()=>u(()=>import("./tls-config.html-By79nyGp.js"),__vite__mapDeps([138,1])),meta:{d:1645407262e3,g:["Vault","https","Configuration","Server"],e:` +Error reading mysql/creds/my-role: Error making API request. +URL: GET http://127.0.0.1:8200/v1/mysql/creds/my-role +Code: 500. Errors: + +* 1 error occurred: + * Error 1470: String 'v-root-my-role-87BP93fheiaHKGelc' is too long for user name (should be no longer than 16) +
+
+- Consul tls create 명령어를 이용하여 인증서 생성, 그외에 사설인증서 만드는 방법으로는 더 테스트 해봐야 할듯
+`,r:{minutes:.41,words:124},y:"a",t:"Vault Server tls 설정"}}],["/04-HashiCorp/06-Vault/06-Config/vault-agent.html",{loader:()=>u(()=>import("./vault-agent.html-BXv48qmS.js"),__vite__mapDeps([139,1])),meta:{d:1656577693e3,g:["Vault","AWS","Configuration","Agent"],e:` +# consul tls create로 인증서 생성 +consul tls ca create -domain=vault -days 3650 +consul tls cert create -domain=vault -dc=global -server -days 3650 +consul tls cert create -domain=vault -dc=global -client -days 3650 +consul tls cert create -domain=vault -dc=global -cli -days 3650 + +# vault config는 아래와 같다. +ui = true + +storage "consul" { + address = "127.0.0.1:8500" + path = "vault/" +} + +listener "tcp" { + address = "0.0.0.0:8200" + #tls_disable = 1 + tls_cert_file = "/root/temp/global-server-vault-0.pem" + tls_key_file = "/root/temp/global-server-vault-0-key.pem" +} + +disable_mlock = true +default_lease_ttl = "768h" +max_lease_ttl = "768h" + +api_addr = "https://172.21.2.50:8200" + +# 명령어를 써야 할 경우 cli 인증서를 export 해줘야한다. +export VAULT_CACERT="\${HOME}/temp/vault-agent-ca.pem" +export VAULT_CLIENT_CERT="\${HOME}/temp/global-cli-vault-0.pem" +export VAULT_CLIENT_KEY="\${HOME}/temp/global-cli-vault-0-key.pem" +
++참고 URL : https://learn.hashicorp.com/tutorials/vault/agent-aws
+`,r:{minutes:2.77,words:831},y:"a",t:"Vault Agent (with aws secret)"}}],["/04-HashiCorp/06-Vault/06-Config/vault-entierprise-license.html",{loader:()=>u(()=>import("./vault-entierprise-license.html-DVI43IYK.js"),__vite__mapDeps([140,1])),meta:{d:1695769332e3,g:["Vault","Enterprise","License"],e:` +`,r:{minutes:.53,words:160},y:"a",t:"Vault +1.12 라이선스"}}],["/04-HashiCorp/06-Vault/07-Sentinel-Sample/aws-secrets-credential-type-check.html",{loader:()=>u(()=>import("./aws-secrets-credential-type-check.html-hCBGD_vi.js"),__vite__mapDeps([141,1])),meta:{d:1689216508e3,g:["Vault","Sentinel","Policy"],e:` +Test ENV
+$ sw_vers +ProductName: macOS +ProductVersion: 12.4 + +$ vault version +Vault v1.11.0 +
1. EGP용 정책 생성 egp_iam_user_deny.sentinel
+`,r:{minutes:.7,words:210},y:"a",t:"AWS Secrets Role Type Check"}}],["/04-HashiCorp/06-Vault/07-Sentinel-Sample/transit-secrets-key-exportable-deny.html",{loader:()=>u(()=>import("./transit-secrets-key-exportable-deny.html-FvvIqCAX.js"),__vite__mapDeps([142,1])),meta:{d:1702878519e3,g:["Vault","Sentinel","Policy"],e:` +import "strings" + +# print(request.data) +credential_type = request.data.credential_type +print("CREDENTIAL_TYPE: ", credential_type) + +allow_role_type = ["federation_token"] + +role_type_check = rule { + credential_type in allow_role_type +} + +# Only check AWS Secret Engine +# Only check create, update +precond = rule { + request.operation in ["create", "update"] +} + +main = rule when precond { + role_type_check +} +
1. EGP용 정책 생성 exportable_deny.sentinel
+`,r:{minutes:.77,words:231},y:"a",t:"Transit Key Exportable Deny"}}],["/04-HashiCorp/07-Nomad/01-Information/nomad-sizing.html",{loader:()=>u(()=>import("./nomad-sizing.html-DYiixYEk.js"),__vite__mapDeps([143,1])),meta:{d:1642218029e3,g:["nomad","sizing"],e:` +import "strings" + +exportable = request.data.exportable + +exportable_check = rule { + exportable is "false" +} + +main = rule { + exportable_check +} +
+`,r:{minutes:.26,words:79},y:"a",t:"Nomad Sizing"}}],["/04-HashiCorp/07-Nomad/01-Information/nomad_job_restart.html",{loader:()=>u(()=>import("./nomad_job_restart.html-DHBvZoL7.js"),__vite__mapDeps([144,1])),meta:{d:1644889869e3,g:["nomad","sizing"],e:` +https://learn.hashicorp.com/tutorials/nomad/production-reference-architecture-vm-with-consul
+Nomad는 Server/Client 구조로 구성되며, Client의 경우 자원사용량이 매우 미미하므로 자원산정은 Server를 기준으로 산정
++`,r:{minutes:1.01,words:304},y:"a",t:"task 복구 방식"}}],["/04-HashiCorp/07-Nomad/02-Config/Cloudwatch-Logging.html",{loader:()=>u(()=>import("./Cloudwatch-Logging.html-B1APZMkF.js"),__vite__mapDeps([145,1])),meta:{d:1639533195e3,g:["Nomad","AWS","Cloudwatch","log"],e:` +원문 : https://www.hashicorp.com/blog/resilient-infrastructure-with-nomad-restarting-tasks
+
+Nomad가 종종 운영자 개입 없이 장애, 중단 상황, Nomad 클러스터 인프라의 유지 관리를 처리하는 방법 확인docker 런타임에는 log driver로 "awslogs"를 지원합니다.
+
+https://docs.docker.com/config/containers/logging/awslogs/+`,r:{minutes:.85,words:256},y:"a",t:"Docker log driver and Cloudwatch on Nomad"}}],["/04-HashiCorp/07-Nomad/02-Config/Namespace.html",{loader:()=>u(()=>import("./Namespace.html-Ch8o5mNT.js"),__vite__mapDeps([146,1])),meta:{d:1628557352e3,g:["Nomad","Namespace"],e:` +팁
+Nomad에서 docker 자체의 로깅을 사용하므로서, Nomad에서 실행되는 docker 기반 컨테이너의 로깅이 특정 환경에 락인되는것을 방지합니다.
+++Nomad Version : >= 1.0.0
+
+Nomad Ent. Version : >= 0.7.0
+https://learn.hashicorp.com/tutorials/nomad/namespacesNamespace 생성
+`,r:{minutes:.38,words:113},y:"a",t:"Nomad Namespace"}}],["/04-HashiCorp/07-Nomad/02-Config/Nomad-Ui-Token.html",{loader:()=>u(()=>import("./Nomad-Ui-Token.html-DDo04qBT.js"),__vite__mapDeps([147,1])),meta:{d:1648772616e3,g:["Nomad","ACL"],e:` +$ nomad namespace apply -description "PoC Application" apps +
++팁
+해당 Token의 policy는 특정인이 원하여 만들었으며, 더 다양한 제약과 허용을 할 수 있습니다. 해당 policy는 아래와 같은 제약과 허용을 합니다.
++
+- UI에서 exec(job에 접근) 제한
+- 그 외에 job, node, volume, server등의 모든 화면 읽어오기
+Nomad cli
+`,r:{minutes:.4,words:121},y:"a",t:"Nomad UI Token"}}],["/04-HashiCorp/07-Nomad/02-Config/Nomad-sslkey-create.html",{loader:()=>u(()=>import("./Nomad-sslkey-create.html-DXTi_C5p.js"),__vite__mapDeps([148,1])),meta:{d:162955514e4,g:["Nomad","SSL"],e:` +#원하는 권한이 있는 policy file +$ cat nomad-ui-policy.hcl +namespace "*" { + policy = "read" + capabilities = ["submit-job", "dispatch-job", "read-logs", "list-jobs", "parse-job", "read-job", "csi-list-volume", "csi-read-volume", "list-scaling-policies", "read-scaling-policy", "read-job-scaling", "read-fs"] +} +node { + policy = "read" +} +host_volume "*" { + policy = "write" +} +plugin { + policy = "read" +} + +#위에서 만든 policy 파일을 nomad cluster에 적용 +$ nomad acl policy apply -description "Production UI policy" prod-ui nomad-ui-policy.hcl + +#해당 policy로 token생성(policy는 여러개를 넣을 수도 있음) +$ nomad acl token create -name="prod ui token" -policy=prod-ui -type=client | tee ui-prod.token +#웹 브라우저 로그인을 위해 Secret ID 복사 +
++팁
+공식 사이트에 consul 인증서 생성 가이드는 있는데 Nomad 인증서 생성가이드는
+
+Show Terminal을 들어가야 볼 수 있기때문에 귀찮음을 해결하기 위해 공유합니다.Nomad 인증서 생성
+`,r:{minutes:.28,words:83},y:"a",t:"Nomad 인증서 생성"}}],["/04-HashiCorp/07-Nomad/02-Config/Server.html",{loader:()=>u(()=>import("./Server.html-C8bYcrnP.js"),__vite__mapDeps([149,1])),meta:{d:162955514e4,g:["Nomad","Enterprise","Configuration","Server"],e:` +consul tls ca create -domain=nomad -days 3650 + +consul tls cert create -domain=nomad -dc=global -server -days 3650 + +consul tls cert create -domain=nomad -dc=global -client -days 3650 + +consul tls cert create -domain=nomad -dc=global -cli -days 3650 +
++팁
+최대한 설정값을 넣어보고, 번역기도 돌려보고 물어도 보고 넣은 server설정 파일입니다.
+
+네트워크는 프라이빗(온프레이머스) 환경입니다.`,r:{minutes:.54,words:161},y:"a",t:"Nomad 서버 설정"}}],["/04-HashiCorp/07-Nomad/02-Config/client.html",{loader:()=>u(()=>import("./client.html-BZaRWunh.js"),__vite__mapDeps([150,1])),meta:{d:162955514e4,g:["Nomad","Enterprise","Configuration","Client"],e:` +#nomad server 설정 +server { + enabled = true + bootstrap_expect = 3 + license_path="/opt/nomad/license/nomad.license" + server_join { + retry_join = ["172.30.1.17","172.30.1.18","172.30.1.19"] + } + raft_protocol = 3 + event_buffer_size = 100 + non_voting_server = false + heartbeat_grace = "10s" +} + + +#tls 설정 +tls { + http = true + rpc = true + + ca_file = "/opt/ssl/nomad/nomad-agent-ca.pem" + cert_file = "/opt/ssl/nomad/global-server-nomad-0.pem" + key_file = "/opt/ssl/nomad/global-server-nomad-0-key.pem" + + #UI오픈할 서버만 변경 + verify_server_hostname = false + verify_https_client = false + #일반서버는 아래와 같이 설정 + verify_server_hostname = true + verify_https_client = true +} +
++팁
+최대한 설정값을 넣어보고, 번역기도 돌려보고 물어도 보고 넣은 Client설정 파일입니다.
+
+네트워크는 프라이빗(온프레이머스) 환경입니다.`,r:{minutes:.62,words:185},y:"a",t:"Nomad 클라이언트 설정"}}],["/04-HashiCorp/07-Nomad/02-Config/common.html",{loader:()=>u(()=>import("./common.html-BFTGzfD3.js"),__vite__mapDeps([151,1])),meta:{d:162955514e4,g:["Nomad","Enterprise","Configuration","Common"],e:` +#nomad client 설정 + +client { + enabled = true + servers = ["172.30.1.17","172.30.1.18","172.30.1.19"] + server_join { + retry_join = ["172.30.1.17","172.30.1.18","172.30.1.19"] + retry_max = 3 + retry_interval = "15s" + } + #host에서 nomad에서 사용할 수 있는 volume 설정 + host_volume "logs" { + path = "/var/logs/elk/" + read_only = false + } + #각각의 client의 레이블 작성 + #meta { + # name = "moon" + # zone = "web" + #} + #nomad에서 예약할 자원 + reserved { + #Specifies the amount of CPU to reserve, in MHz. + cpu = 200 + #Specifies the amount of memory to reserve, in MB. + memory = 8192 + #Specifies the amount of disk to reserve, in MB. + disk = 102400 + } + no_host_uuid = true + #bridge network interface name + bridge_network_name = "nomad" + bridge_network_subnet = "172.26.64.0/20" + cni_path = "/opt/cni/bin" + cni_config_dir = "/opt/cni/config" +} +#tls 설정 +tls { + http = true + rpc = true + + ca_file = "/opt/ssl/nomad/nomad-agent-ca.pem" + cert_file = "/opt/ssl/nomad/global-client-nomad-0.pem" + key_file = "/opt/ssl/nomad/global-client-nomad-0-key.pem" + + verify_server_hostname = true + verify_https_client = true +} +
++팁
+최대한 설정값을 넣어보고, 번역기도 돌려보고 물어도 보고 넣은 server, client의 공통설정 파일입니다.
+
+저는 agent.hcl파일안에 다 넣고 실행하지만 나눠서 추후에는 기능별로 나눠서 사용할 예정입니다.`,r:{minutes:.43,words:130},y:"a",t:"Nomad 공통 설정"}}],["/04-HashiCorp/07-Nomad/02-Config/consul-namespace.html",{loader:()=>u(()=>import("./consul-namespace.html-DVrju3c2.js"),__vite__mapDeps([152,1])),meta:{d:1630463356e3,g:["Nomad","Enterprise","Consul"],e:` +#nomad 공통 설정 +datacenter = "dc1" +region = "global" +data_dir = "/opt/nomad/nomad" +bind_addr = "{{ GetInterfaceIP \`ens192\` }}" + +advertise { + # Defaults to the first private IP address. + #http = "{{ GetInterfaceIP \`ens244\` }}" + #rpc = "{{ GetInterfaceIP \`ens244\` }}" + #serf = "{{ GetInterfaceIP \`ens244\` }}" + http = "{{ GetInterfaceIP \`ens192\` }}" + rpc = "{{ GetInterfaceIP \`ens192\` }}" + serf = "{{ GetInterfaceIP \`ens192\` }}" +} + +consul { + address = "127.0.0.1:8500" + server_service_name = "nomad" + client_service_name = "nomad-client" + auto_advertise = true + server_auto_join = true + client_auto_join = true + #consul join용 token + token = "33ee4276-e1ef-8e5b-d212-1f94ca8cf81e" +} +enable_syslog = false +enable_debug = false +disable_update_check = false + +log_level = "DEBUG" +log_file = "/var/log/nomad/nomad.log" +log_rotate_duration = "24h" +log_rotate_bytes = 104857600 +log_rotate_max_files = 100 + +ports { + http = 4646 + rpc = 4647 + serf = 4648 +} + +#prometheus에서 nomad의 metrics값을 수집 해 갈 수 있게 해주는 설정 +telemetry { + collection_interval = "1s" + disable_hostname = true + prometheus_metrics = true + publish_allocation_metrics = true + publish_node_metrics = true +} + + +plugin "docker" { + config { + auth { + config = "/root/.docker/config.json" + } + #온프레이머스환경에서는 해당 이미지를 private repository에 ㅓㄶ고 변경 + infra_image = "google-containers/pause-amd64:3.1" + } +} + +acl { + enabled = true +} +
Job의 Consul Namespace 정의
+Consul Enterprise는
+Namespace
가 있어서 Nomad로 Consul에 서비스 등록 시 특정 Namespace를 지정할 수 있음Job > Group > Consul
+`,r:{minutes:2.79,words:838},y:"a",t:"Consul namespace 사용시 Nomad의 서비스 등록"}}],["/04-HashiCorp/07-Nomad/02-Config/csi-nfs.html",{loader:()=>u(()=>import("./csi-nfs.html-lgxMjeBE.js"),__vite__mapDeps([153,1])),meta:{d:1639892483e3,g:["Nomad","config","csi","nfs"],e:` +job "frontback_job" { + group "backend_group_v1" { + + count = 1 + + consul { + namespace = "mynamespace" + } + + service { + name = "backend" + port = "http" + + connect { + sidecar_service {} + } + + check { + type = "http" + path = "/" + interval = "5s" + timeout = "3s" + } + } +# 생략 +
+
+- nomad에서 외부 storage를 사용하기 위한 plugin +
++
+- 그 중에서도 접근성이 좋은 nfs를 사용, public cloud에서 제공하는 storage와는 사용법이 다를 수 있음
+- 구성환경은 아래와 같다.(사실 nfs server정보만 보면 될 거 같음) +
++
+- nfs-server 10.0.0.151:/mnt/data
+controller
++
`,r:{minutes:.69,words:207},y:"a",t:"nomad csi (nfs)"}}],["/04-HashiCorp/07-Nomad/02-Config/custom-ui-link.html",{loader:()=>u(()=>import("./custom-ui-link.html-BlrHvl1y.js"),__vite__mapDeps([154,1])),meta:{d:1661946241e3,g:["Nomad","UI"],e:` +- 하나이상의 node에 storage를 배포할 수 있게 해주는 중앙관리 기능
+- 어느 node(client)에 띄어져도 상관없다.
++ ++Nomad ui 설정에 다음과 같이 Consul과 Vault의 링크를 추가할 수 있습니다.
+`,r:{minutes:.14,words:41},y:"a",t:"Nomad UI에 Consul과 Vault 링크 추가"}}],["/04-HashiCorp/07-Nomad/02-Config/integrateVault.html",{loader:()=>u(()=>import("./integrateVault.html-CLUbyXkX.js"),__vite__mapDeps([155,1])),meta:{d:1642495601e3,g:["Nomad","Namespace"],e:` +ui { + enabled = true + + consul { + ui_url = "https://consul.example.com:8500/ui" + } + + vault { + ui_url = "https://vault.example.com:8200/ui" + } +} +
아래 작업 전 Forward DNS for Consul Service Discovery을 진행해야함
++
+- Consul 설정 링크
+예제를 위한 vault kv 설정
+`,r:{minutes:.73,words:220},y:"a",t:"integrate Vault"}}],["/04-HashiCorp/07-Nomad/02-Config/nomad-guide-basic.html",{loader:()=>u(()=>import("./nomad-guide-basic.html-BqDBn4kt.js"),__vite__mapDeps([156,1])),meta:{d:1652660873e3,g:["Nomad","Sample"],e:` +# 사용된 policy들 +$ cat nomad-cluster-role.json +{ + "allowed_policies": "admin", + "token_explicit_max_ttl": 0, + "name": "nomad-cluster", + "orphan": true, + "token_period": 259200, + "renewable": true +} +vault write /auth/token/roles/nomad-cluster @nomad-cluster-role.json + +$ cat admin-policy.hcl +# Read system health check +path "sys/health" +{ + capabilities = ["read", "sudo"] +} + +# Create and manage ACL policies broadly across Vault + +# List existing policies +path "sys/policies/acl" +{ + capabilities = ["list"] +} + +# Create and manage ACL policies +path "sys/policies/acl/*" +{ + capabilities = ["create", "read", "update", "delete", "list", "sudo"] +} + +# Enable and manage authentication methods broadly across Vault + +# Manage auth methods broadly across Vault +path "auth/*" +{ + capabilities = ["create", "read", "update", "delete", "list", "sudo"] +} + +# Create, update, and delete auth methods +path "sys/auth/*" +{ + capabilities = ["create", "update", "delete", "sudo"] +} + +# List auth methods +path "sys/auth" +{ + capabilities = ["read"] +} + +# Enable and manage the key/value secrets engine at \`secret/\` path + +# List, create, update, and delete key/value secrets +path "secret/*" +{ + capabilities = ["create", "read", "update", "delete", "list", "sudo"] +} + +# Manage secrets engines +path "sys/mounts/*" +{ + capabilities = ["create", "read", "update", "delete", "list", "sudo"] +} + +# List existing secrets engines. +path "sys/mounts" +{ + capabilities = ["read"] +} + +vault policy write admin admin-policy.hcl + +# token 생성 +vault token create -policy admin -period 72h -orphan +
Download
++
+- Release link : https://releases.hashicorp.com/nomad
+- Version : 1.3.x
+- Platform 선택 +
++
+- darwin(= MAC)
+- Linux
+- Windows
+on Linux
`,r:{minutes:2.74,words:823},y:"a",t:"Nomad Guide - Basic"}}],["/04-HashiCorp/07-Nomad/02-Config/nomad-on-windows.html",{loader:()=>u(()=>import("./nomad-on-windows.html-Co-Gyxsw.js"),__vite__mapDeps([157,1])),meta:{d:1658932718e3,g:["Nomad","Windows"],e:` +Nomad를 Windows환경에 구성하고 실행을위해 서비스로 등록하는 방법을 알아봅니다. 솔루션 실행 환경 또는 운영/개발자의 익숙함 정도에 따라 다양한 OS를 선택하여 애플리케이션을 배포하게 됩니다. Nomad를 통해 배포를 위한 오케스트레이터를 Windows 환경에 적용하고 서비스에 등록하여 상시적으로 실행될 수 있도록하는 구성을 안내합니다.
+Port 구성
++`,r:{minutes:2.35,words:704},y:"a",t:"Nomad on Windows"}}],["/04-HashiCorp/07-Nomad/04-UseCase/jenkins-pipeline.html",{loader:()=>u(()=>import("./jenkins-pipeline.html-hwIFCVL_.js"),__vite__mapDeps([158,1])),meta:{d:1656411375e3,g:["Nomad","Jenkins","Java","Docker","Vault"],e:` +참고 url : Port used
+Test ENV
+`,r:{minutes:5.52,words:1656},y:"a",t:"Jenkins Pipeline Nomad (Integrated Vault)"}}],["/04-HashiCorp/07-Nomad/04-UseCase/job-start-from-hcl.html",{loader:()=>u(()=>import("./job-start-from-hcl.html-BwGQHsUl.js"),__vite__mapDeps([159,1])),meta:{d:167721987e4,g:["Nomad","API","HCL"],e:` +$ sw_vers +ProductName: macOS +ProductVersion: 12.4 + +$ brew --version +Homebrew 3.5.2 + +$ git version +git version 2.27.0 + +$ java -version +openjdk version "11.0.14.1" 2022-02-08 + +$ gradle --version +Welcome to Gradle 7.4.2! + +$ docker version +Client: + Version: 20.10.9 + +Server: + Engine: + Version: 20.10.14 + +$ vault version +Vault v1.11.0 + +$ nomad version +Nomad v1.3.1 + +$ curl --version +curl 7.79.1 (x86_64-apple-darwin21.0) + +$ aws --version +aws-cli/2.7.11 Python/3.10.5 Darwin/21.5.0 source/x86_64 prompt/off +
HCL로 작성된 Job의 경우 Nomad CLI 또는 UI 접속이 가능하다면 바로 적용 가능하다.
+`,r:{minutes:1.01,words:304},y:"a",t:"Pass HCL to API"}}],["/04-HashiCorp/07-Nomad/04-UseCase/springboot-graceful-shutdown.html",{loader:()=>u(()=>import("./springboot-graceful-shutdown.html-C0Rq6jUd.js"),__vite__mapDeps([160,1])),meta:{d:1673502484e3,g:["Nomad","SpringBoot","Java"],e:` +HCL Job Sample (2048.hcl)
+job "2048-game" { + datacenters = ["dc1"] + type = "service" + group "game" { + count = 1 # number of instances + + network { + port "http" { + static = 80 + } + } + + task "2048" { + driver = "docker" + + config { + image = "alexwhen/docker-2048" + + ports = [ + "http" + ] + + } + + resources { + cpu = 500 # 500 MHz + memory = 256 # 256MB + } + } + } +} +
++GitHub 리소스 : https://github.com/Great-Stone/nomad-springboot-graceful-shutdown
+테스트 환경
++
`,r:{minutes:1.67,words:502},y:"a",t:"Graceful Shutdown 적용 (kill_signal)"}}],["/04-HashiCorp/07-Nomad/05-SampleJob/DAS.html",{loader:()=>u(()=>import("./DAS.html-BoEa47a0.js"),__vite__mapDeps([161,1])),meta:{d:1664763179e3,g:["Nomad","sample","job","autoscaler","das"],e:` +- Gradle 7.4.2
+- Java 11
+- Spring Boot 2.7.7
+- Nomad 1.4.3
++
+- Nomad autoscaler 배포 후 사용할 수 있는 기능 중에 하나
+- Dynamic application sizing(DAS)의 기능이 설정되어 있는 job을 배포 한 이후 autoscaler job에서 resource의 권고를 받아올 수 있음
+- 권고 받은 값을 사용자가 확인 후 허용할 경우 job의 resource의 변화가 정상적으로 적용됨
+autoscaler job은 기존에 사용하던 job을 사용
+`,r:{minutes:1.23,words:370},y:"a",t:"Dynamic application sizing"}}],["/04-HashiCorp/07-Nomad/05-SampleJob/VaultSWLB-nginx.html",{loader:()=>u(()=>import("./VaultSWLB-nginx.html-Ba3IVIOo.js"),__vite__mapDeps([162,1])),meta:{d:164543379e4,g:["Nomad","Sample","Job","Vault","SWLB"],e:` ++
+- Vault의 HA구성 시에는 LB가 필요한데, LB대용으로 SWLB를 이용하여 Vault를 사용할 수 있다. +
++
+- 해당 페이지에서는 nginx를 사용하였지만, HAproxy도 비슷하게 사용이 가능하다.
+nginx job 파일
+`,r:{minutes:.53,words:159},y:"a",t:"Vault SWLB용도의 nginx"}}],["/04-HashiCorp/07-Nomad/05-SampleJob/autoscaler.html",{loader:()=>u(()=>import("./autoscaler.html--G0q8-Ol.js"),__vite__mapDeps([163,1])),meta:{d:1644990252e3,g:["Nomad","sample","job","autoscaler","aws"],e:` +job "nginx" { + datacenters = ["dc1"] + + group "nginx" { + + constraint { + attribute = "${attr.unique.hostname}" + value = "slave0" + } + + #Vault tls가 있고 nomad client hcl 파일에 host volume이 명시되어 있는 설정 값 + volume "cert-data" { + type = "host" + source = "cert-data" + read_only = false + } + + #실패 없이 되라고 행운의 숫자인 7을 4번 줌 + network { + port "http" { + to = 7777 + static = 7777 + } + } + + service { + name = "nginx" + port = "http" + } + + task "nginx" { + driver = "docker" + + volume_mount { + volume = "cert-data" + destination = "/usr/local/cert" + } + + config { + image = "nginx" + + ports = ["http"] + volumes = [ + "local:/etc/nginx/conf.d", + + ] + } + template { + data = <<EOF +#Vault는 active서버 1대외에는 전부 standby상태이며 +#서비스 호출 시(write)에는 active 서비스만 호출해야함으로 아래와 같이 consul에서 서비스를 불러옴 + +upstream backend { +{{ range service "active.vault" }} + server {{ .Address }}:{{ .Port }}; +{{ else }}server 127.0.0.1:65535; # force a 502 +{{ end }} +} + +server { + listen 7777 ssl; + #위에서 nomad host volume을 mount한 cert를 가져옴 + ssl on; + ssl_certificate /usr/local/cert/vault/global-client-vault-0.pem; + ssl_certificate_key /usr/local/cert/vault/global-client-vault-0-key.pem; + #vault ui 접근 시 / -> /ui redirect되기 때문에 location이 /외에는 되지 않는다. + location / { + proxy_pass https://backend; + } +} +EOF + + destination = "local/load-balancer.conf" + change_mode = "signal" + change_signal = "SIGHUP" + } + resources { + cpu = 100 + memory = 201 + } + } + } +} +
+
`,r:{minutes:1.2,words:361},y:"a",t:"Autoscaler"}}],["/04-HashiCorp/07-Nomad/05-SampleJob/code-server.html",{loader:()=>u(()=>import("./code-server.html-Cf6r2Y5O.js"),__vite__mapDeps([164,1])),meta:{d:1669181764e3,g:["Nomad","Sample","Job","vs-code"],e:` +- aws cloud 환경에서 별도의 모니터링 툴을 사용하지 않고 nomad-apm 기능을 이용한 auscaler 구성
+- Nomad Autoscaler 다운로드 : + +
+- 주요링크 +
++
+- Nomad Autoscaler aws IAM policy 관련 : https://www.nomadproject.io/tools/autoscaling/plugins/target/aws-asg
+- Nomad Autoscaler policy 관련 : https://www.nomadproject.io/tools/autoscaling/policy
+- Nomad Autoscaler의 nomad-apm 을 사용하는 경우 : https://www.nomadproject.io/tools/autoscaling/plugins/apm/nomad
++
+- vs-code를 local이 아닌 환경에서 사용할 수 있도록 도와주는 code-server의 예시입니다.
+- 이 code의 변수는 nomad variable를 활용해서 배포합니다.
+변수 선언
++
+- web ui 접근 password와 code-server terminal에서 사용 할 sudo의 password 를 변수로 선언했습니다.
+`,r:{minutes:.41,words:122},y:"a",t:"code-server"}}],["/04-HashiCorp/07-Nomad/05-SampleJob/elastic.html",{loader:()=>u(()=>import("./elastic.html-BMFJjvfm.js"),__vite__mapDeps([165,1])),meta:{d:1632449108e3,g:["Nomad","Sample","Job"],e:` +# nomad var put {path기반의 varialbes} key=vaule +$ nomad var put code/config password=password +
`,r:{minutes:.39,words:118},y:"a",t:"Elastic"}}],["/04-HashiCorp/07-Nomad/05-SampleJob/elk_version7.html",{loader:()=>u(()=>import("./elk_version7.html-Cf6byBKT.js"),__vite__mapDeps([166,1])),meta:{d:1642040133e3,g:["Nomad","Sample","Job"],e:` +job "elastic" { + datacenters = ["dc1"] + + group "elastic" { + network { + port "db" { + static = 9200 + } + port "kibana" { + static = 5601 + } + } + + service { + port = "db" + + check { + type = "tcp" + interval = "10s" + timeout = "2s" + } + } + + task "elasticsearch" { + driver = "docker" + + config { + image = "docker.elastic.co/elasticsearch/elasticsearch:6.8.9" + ports = ["db"] + mount = [ + { + type = "bind" + source = "local/elasticsearch.yml" + target = "/usr/share/elasticsearch/config/elasticsearch.yml" + } + ] + } + + template { + data = <<EOH +network.host: 0.0.0.0 +discovery.seed_hosts: ["127.0.0.1"] +xpack.security.enabled: true +xpack.license.self_generated.type: trial +xpack.monitoring.collection.enabled: true +EOH + destination = "local/elasticsearch.yml" + } + + env { + # "discovery.type":"single-node", + ES_JAVA_OPTS = "-Xms512m -Xmx1024m" + } + + resources { + cpu = 2000 + memory = 1024 + } + } + + task "kibana" { + driver = "docker" + + config { + image = "docker.elastic.co/kibana/kibana:6.8.9" + ports = ["kibana"] + mount = [ + { + type = "bind" + source = "local/kibana.yml" + target = "/usr/share/kibana/config/kibana.yml" + } + ] + } + + template { + data = <<EOH +server.name: kibana +elasticsearch.username: elastic +elasticsearch.password: elastic +EOH + destination = "local/kibana.yml" + } + + env { + ELASTICSEARCH_URL="http://172.28.128.31:9200" + } + + resources { + cpu = 1000 + memory = 1024 + } + } + } +} + +
7버전에 elsaticsearch에서 기본패스워드를 찾지 못해서 env로 넣어줌
+logstash는 적당한 샘플을 찾지 못해서 일단은 비워둠
+`,r:{minutes:.79,words:236},y:"a",t:"elk_version7"}}],["/04-HashiCorp/07-Nomad/05-SampleJob/ingress-gateway.html",{loader:()=>u(()=>import("./ingress-gateway.html-9SiCbfk_.js"),__vite__mapDeps([167,1])),meta:{d:164015281e4,g:["Nomad","Sample","Job",""],e:` +job "elk" { + + datacenters = ["dc1"] + + constraint { + attribute = "${attr.kernel.name}" + value = "linux" + } + + update { + stagger = "10s" + max_parallel = 1 + } + + group "elk" { + count = 1 + + restart { + attempts = 2 + interval = "1m" + delay = "15s" + mode = "delay" + } + network { + port "elastic" { + to = 9200 + static = 9200 + } + port "kibana" { + to = 5601 + } + port "logstash" { + to = 5000 + } + } + + task "elasticsearch" { + driver = "docker" + + constraint { + attribute = "${attr.unique.hostname}" + value = "slave2" + } + + config { + image = "elasticsearch:7.16.2" + ports = ["elastic"] + volumes = [ + "local/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml", + ] + } + template { + data = <<EOF +cluster.name: "my-cluster" +network.host: 0.0.0.0 +discovery.type: single-node +discovery.seed_hosts: ["127.0.0.1"] +xpack.security.enabled: true +xpack.license.self_generated.type: trial +xpack.monitoring.collection.enabled: true +EOF + destination = "local/elasticsearch.yml" + change_mode = "signal" + change_signal = "SIGHUP" + } + env { + ELASTIC_PASSWORD = "elastic" + } + + service { + name = "${TASKGROUP}-elasticsearch" + port = "elastic" + } + + resources { + cpu = 500 + memory = 2048 + } + } + + task "kibana" { + driver = "docker" + + constraint { + attribute = "${attr.unique.hostname}" + value = "slave2" + } + + config { + image = "kibana:7.16.2" + ports = ["kibana"] + volumes = [ + "local/kibana.yml:/usr/share/kibana/config/kibana.yml" + ] + } + template { + data = <<EOF +# +# ** THIS IS AN AUTO-GENERATED FILE ** +# + +# Default Kibana configuration for docker target +server.host: "0.0.0.0" +server.shutdownTimeout: "5s" +elasticsearch.hosts: [ "http://{{ env "NOMAD_IP_elk" }}:{{ env "NOMAD_PORT_elk" }}" ] +elasticsearch.username: elastic +elasticsearch.password: elastic +EOF + + destination = "local/kibana.yml" + change_mode = "signal" + change_signal = "SIGHUP" + } + + service { + name = "${TASKGROUP}-kibana" + port = "kibana" + check { + type = "http" + path = "/" + interval = "10s" + timeout = "2s" + } + } + + resources { + cpu = 500 + memory = 1200 + } + } + + task "logstash" { + driver = "docker" + + constraint { + attribute = "${attr.unique.hostname}" + value = "slave2" + } + + config { + image = "logstash:7.16.2" + ports = ["logstash"] + volumes = [ + "local/logstash.yml:/usr/share/logstash/config/logstash.yml" + ] + } + template { + data = <<EOF +http.host: "0.0.0.0" +xpack.monitoring.elasticsearch.hosts: [ "http://{{ env "NOMAD_IP_elk" }}:{{ env "NOMAD_PORT_elk" }}" ] +EOF + + destination = "local/logstash.yml" + change_mode = "signal" + change_signal = "SIGHUP" + } + + service { + name = "${TASKGROUP}-logstash" + port = "logstash" + } + + resources { + cpu = 200 + memory = 1024 + } + } + } +} + +
Nomad job으로 ingress gateway 사용하기 (with consul)
++
+- consul로 설정하는 ingress gateway가 아닌 Nomad job 기동 시에 ingress gateway 활성화 예제 +
++
+- hashicorp 공식 예제가 아닌 다른 걸 해보려하다, 특이한 부분을 확인함
+테스트 job (python fastapi)
+`,r:{minutes:.56,words:169},y:"a",t:"Nomad ingress gateway"}}],["/04-HashiCorp/07-Nomad/05-SampleJob/install-ansible-docker.html",{loader:()=>u(()=>import("./install-ansible-docker.html-DRjH0VEL.js"),__vite__mapDeps([168,1])),meta:{d:1693402327e3,g:["Nomad","Ansible","Job","Docker"],e:` +job "22-fastapi" { + datacenters = ["dc1"] + + group "fastapi" { + + network { + mode = "bridge" + #service가 80으로 뜸, 만약 다른 포트로 뜨는 서비스를 사용 할 경우 image와 to만 변경 + port "http" { + to = 80 + } + } + + service { + name = "fastapi" + #여기서 port에 위에서 미리 선언한 http를 쓸 경우 다이나믹한 port를 가져오는데 + #그럴 경우 ingress gateway에서 못 읽어 온다. + port = "80" + connect { + sidecar_service{} + } + } + + task "fastapi" { + driver = "docker" + + config { + image = "tiangolo/uvicorn-gunicorn-fastapi" + ports = ["http"] + } + + resources { + cpu = 500 + memory = 282 + } + } + scaling { + enabled = true + min = 1 + max = 3 + + policy { + evaluation_interval = "5s" + cooldown = "1m" + #driver = "nomad-apm" + check "mem_allocated_percentage" { + source = "nomad-apm" + query = "max_memory" + + strategy "target-value" { + target = 80 + } + } + } + } + } +} + +
++참고 : https://discuss.hashicorp.com/t/escape-characters-recognized-as-a-variable-in-template-stanza/40525
+Nomad를 통해 Ops작업을 수행할 때
`,r:{minutes:.84,words:252},y:"a",t:"Nomad에서 Ansible로 Docker 설치와 Template 주의사항"}}],["/04-HashiCorp/07-Nomad/05-SampleJob/jboss.html",{loader:()=>u(()=>import("./jboss.html-BXExyhGy.js"),__vite__mapDeps([169,1])),meta:{d:1639892483e3,g:["Nomad","Sample","Job","wildfly","JBoss"],e:` +sysbatch
타입의 Job에 Ansible을raw_exec
로 실행하면 전체 노드에서 일괄로 작업을 수행할 수 있다.+`,r:{minutes:1.05,words:314},y:"a",t:"Wildfly(Jboss)"}}],["/04-HashiCorp/07-Nomad/05-SampleJob/jenkins_java_driver.html",{loader:()=>u(()=>import("./jenkins_java_driver.html-hGi3Yh8x.js"),__vite__mapDeps([170,1])),meta:{d:166773544e4,g:["Nomad","Sample","Job","Jenkins"],e:` +image info : https://quay.io/repository/wildfly/wildfly?tab=info
+
+github : https://github.com/jboss-dockerfiles/wildfly
+wildfly docker example : http://www.mastertheboss.com/soa-cloud/docker/deploying-applications-on-your-docker-wildfly-image/+`,r:{minutes:1.29,words:387},y:"a",t:"Jenkins(Java Driver) on Nomad"}}],["/04-HashiCorp/07-Nomad/05-SampleJob/keycloak.html",{loader:()=>u(()=>import("./keycloak.html-CaOYyGHJ.js"),__vite__mapDeps([171,1])),meta:{d:16324491e5,g:["Nomad","Sample","Job"],e:` +Nomad
++
+- Java Driver : https://developer.hashicorp.com/nomad/docs/drivers/java
+- Schecuduler Config : https://developer.hashicorp.com/nomad/api-docs/operator/scheduler
+Keycloak은 Stateful 한 특성이 있어서 볼륨을 붙여주는것이 좋다.
+`,r:{minutes:.31,words:93},y:"a",t:"Keycloak"}}],["/04-HashiCorp/07-Nomad/05-SampleJob/mongodb.html",{loader:()=>u(()=>import("./mongodb.html-MginIp_a.js"),__vite__mapDeps([172,1])),meta:{d:1632449108e3,g:["Nomad","Sample","Job"],e:` +// nomad namespace apply -description "Keycloak" keycloak + +job "keycloak" { + type = "service" + datacenters = ["dc1"] + namespace = "keycloak" + + group "keycloak" { + count = 1 + + volume "keycloak-vol" { + type = "host" + read_only = false + source = "keycloak-vol" + } + + task "keycloak" { + driver = "docker" + + volume_mount { + volume = "keycloak-vol" + destination = "/opt/jboss/keycloak/standalone/data" + read_only = false + } + + config { + image = "quay.io/keycloak/keycloak:14.0.0" + port_map { + keycloak = 8080 + callback = 8250 + } + } + + env { + KEYCLOAK_USER = "admin" + KEYCLOAK_PASSWORD = "admin" + } + + resources { + memory = 550 + + network { + port "keycloak" { + static = 18080 + } + port "callback" { + static = 18250 + } + } + } + + service { + name = "keycloak" + tags = ["auth"] + + check { + type = "tcp" + interval = "10s" + timeout = "2s" + port = "keycloak" + } + } + } + } +} +
`,r:{minutes:.17,words:50},y:"a",t:"MongoDB"}}],["/04-HashiCorp/07-Nomad/05-SampleJob/nexus.html",{loader:()=>u(()=>import("./nexus.html-CbAkU-gG.js"),__vite__mapDeps([173,1])),meta:{d:1656325647e3,g:["Nomad","Sample","Job"],e:` +job "mongodb" { + datacenters = ["dc1"] + + group "mongodb" { + network { + port "db" { + static = 27017 + } + } + + service { + port = "db" + + check { + type = "tcp" + interval = "10s" + timeout = "2s" + } + } + + task "mongodb" { + driver = "docker" + + config { + image = "mongo:3.6.21" + ports = ["db"] + } + + env { + MONGO_INITDB_ROOT_USERNAME = "admin" + MONGO_INITDB_ROOT_PASSWORD = "password" + } + + resources { + cpu = 2000 + memory = 1024 + } + } + } +} +
`,r:{minutes:.16,words:49},y:"a",t:"Nexus"}}],["/04-HashiCorp/07-Nomad/05-SampleJob/nginx.html",{loader:()=>u(()=>import("./nginx.html-L9iFTT7o.js"),__vite__mapDeps([174,1])),meta:{d:1633418665e3,g:["Nomad","Sample","Job","reverse proxy","consul service discovery"],e:` +job "nexus" { + datacenters = ["dc1"] + + group "nexus" { + count = 1 + + network { + port "http" { + to = 8081 + static = 8081 + } + } + + task "nexus" { + driver = "docker" + + config { + image = "sonatype/nexus3" + ports = ["http"] + } + + env { + INSTALL4J_ADD_VM_PARAMS = "-Xms2703m -Xmx2703m -XX:MaxDirectMemorySize=2703m -Djava.util.prefs.userRoot=/some-other-dir" + } + + resources { + cpu = 1000 + memory = 2703 + } + } + } +} +
+
+- nomad와 consul로 cluster로 구성되어 있는 환경에 L4이후에 nomad로 LB를 해야할 경우 사용 +
++
+- nginx server_name설정에서 도메인을 받아오고 location에서는 각각의 서브도메인 별로 reverse proxy 동작 +
++
+- reverse proxy(up stream)에서는 각각의 consul의 등록된 서비스 호출
+`,r:{minutes:.67,words:200},y:"a",t:"nginx"}}],["/04-HashiCorp/07-Nomad/05-SampleJob/nomad-pack.html",{loader:()=>u(()=>import("./nomad-pack.html-CG6ZvRfF.js"),__vite__mapDeps([175,1])),meta:{d:16395807e5,g:["Nomad","Sample","Job","nomad-pack","vuepress"],e:` +job "nginx" { + datacenters = ["dc1"] + + group "nginx" { + //인증서는 host volume에 업로드 + volume "certs" { + type = "host" + source = "certs" + read_only = true + } + + network { + port "http" { + static = 80 + to = 80 + } + port "https" { + to = 443 + static = 443 + } + } + + service { + name = "nginx" + port = "http" + tags = ["web"] + check { + type = "tcp" + port = "http" + interval = "2s" + timeout = "2s" + } + } + + task "server" { + + driver = "docker" + + volume_mount { + volume = "certs" + destination = "/etc/nginx/certs" + } + + config { + image = "nginx" + ports = ["http","https"] + #ports = ["http","https"] + volumes = [ + "local:/etc/nginx/conf.d", + ] + } + + template { + data = <<EOF + +upstream login.shoping.co.kr { +{{ range service "login" }} + server {{ .Address }}:{{ .Port }}; +{{ else }}server 127.0.0.1:65535; # force a 502 +{{ end }} +} +upstream singup.shoping.co.kr { +{{ range service "signup" }} + server {{ .Address }}:{{ .Port }}; +{{ else }}server 127.0.0.1:65535; # force a 502 +{{ end }} +} +upstream main.shoping.co.kr { +{{ range service "main" }} + server {{ .Address }}:{{ .Port }}; +{{ else }}server 127.0.0.1:65535; # force a 502 +{{ end }} +} + +server { + listen 80; + listen 443 ssl; + //domain 및 subdomain호출 + server_name *.shoping.co.kr; + ssl_certificate "/etc/nginx/certs/server.pem"; + ssl_certificate_key "/etc/nginx/certs/server.key"; + proxy_read_timeout 300; + proxy_buffers 64 16k; + + //각 sub도메인별 + location / { + if ($host = login.shoping.co.kr) { + proxy_pass login.shoping.co.kr; + } + if ($host = singup.shoping.co.kr) { + proxy_pass singup.shoping.co.kr; + } + if ($host !~ "(.*main)") { + proxy_pass main.shoping.co.kr; + } + } +} + + + +EOF + + destination = "local/load-balancer.conf" + change_mode = "signal" + change_signal = "SIGHUP" + } + resources { + cpu = 2000 + memory = 2000 + } + } + } +} + + +
+
`,r:{minutes:1.76,words:527},y:"a",t:"nomad-pack custom registry"}}],["/04-HashiCorp/07-Nomad/05-SampleJob/oracleXE.html",{loader:()=>u(()=>import("./oracleXE.html-D0RbxaR-.js"),__vite__mapDeps([176,1])),meta:{d:1632449108e3,g:["Nomad","Sample","Job"],e:` +- nomad job파일을 템플릿처럼 다룰 수 있게 해주는 고마운 기능 +
++
+- nomad-pack custom 메뉴얼 주소 및 커뮤니티 registry +
++
+- nomad-pack: https://github.com/hashicorp/nomad-pack/blob/main/docs/writing-packs.md
+- 커뮤니티 registry : https://github.com/hashicorp/nomad-pack-community-registry
+- 해당 예제는 Vue.js의 vuepress기반의 컨테이너 +
++
+- 참조링크 +
++
+- gitlab: https://gitlab.com/swbs9000/vuepress
+- docker: https://hub.docker.com/repository/docker/swbs90/vuepress CLI: docker push swbs90/vuepress:0.0.3
+- vuepress: https://github.com/docmoa/docs
+`,r:{minutes:.2,words:59},y:"a",t:"Oracle XE"}}],["/04-HashiCorp/07-Nomad/05-SampleJob/param-batch-job.html",{loader:()=>u(()=>import("./param-batch-job.html-b8M0pZxI.js"),__vite__mapDeps([177,1])),meta:{d:1640585468e3,g:["Nomad","Sample","Job","param","batch"],e:` +job "oracle" { + datacenters = ["dc1"] + + group "oracle" { + network { + port "db" { + static = 1521 + } + port "manage" { + static = 5500 + } + } + + service { + port = "db" + + check { + type = "tcp" + interval = "10s" + timeout = "2s" + } + } + + task "oracle" { + driver = "docker" + + config { + image = "oracle/database:18.4.0-xe" + ports = ["db", "manage"] + } + + env { + DB_MEMORY = "2GB" + ORACLE_PWD = "password" + ORACLE_SID = "XE" + } + + resources { + cpu = 2000 + memory = 1024 + } + } + } +} +
+
+- parameter를 받아와서 해당 값을 이용하여 다음으로 실행될 job의 값을 다이나믹하게 넣어 만드는 샘플 +
++
+- meta_required에 원하는 값을 넣고 template에 env "NOMAD_META_메타명(key)"와 같이 넣어주면 된다.
+`,r:{minutes:.32,words:97},y:"a",t:"param-batch-job"}}],["/04-HashiCorp/07-Nomad/05-SampleJob/redis.html",{loader:()=>u(()=>import("./redis.html-CxghhVv_.js"),__vite__mapDeps([178,1])),meta:{d:1633418665e3,g:["Nomad","Sample","Job"],e:` +job "24-paramete" { + datacenters = ["dc1"] + type = "batch" + + parameterized { + payload = "forbidden" + meta_required = ["room_num"] + } + + group "run-main-job" { + + task "run-main-job" { + driver = "raw_exec" + + config { + command = "nomad" + # arguments + args = ["job", "run", "${NOMAD_TASK_DIR}/room.job" ] + } + template { + data = <<EOH + +##################### + +job "{{ env "NOMAD_META_room_num" }}" { + datacenters = ["dc1"] + + group "jboss" { + + network { + port "http" { + to = "8080" + } + } + service { + port = "http" + name = "{{ env "NOMAD_META_room_num" }}" + check { + type = "tcp" + interval = "10s" + timeout = "2s" + } + } + task "http" { + driver = "docker" + config { + image = "jboss/wildfly" + ports = ["http"] + } + resources { + cpu = 500 + memory = 282 + } + } + } +} + +EOH + destination = "local/room.job" + } + } + } +} +
+
`,r:{minutes:.85,words:255},y:"a",t:"redis"}}],["/04-HashiCorp/07-Nomad/05-SampleJob/scouter.html",{loader:()=>u(()=>import("./scouter.html-N24lYJY6.js"),__vite__mapDeps([179,1])),meta:{d:1639485865e3,g:["Nomad","Consul","Scouter","Job"],e:` +- 추가내용: redis는 data dir, cluster info dir(클러스터 시) 이 두개의 dir가 필요하여 volume을 추가로 붙여줘야한다. +
++
+- data dir을 변경이 번거로움(docker build를 다시 해야함) 그래서 클러스터 시에는 docker build, nomad volume으로 나눔과 같은 방법이 있음
+- cluster-enabled : 클러스터로 사용합니다. (cluster volume으로 빼둬야됨)
+- cluster-config-file : 노드별로 클러스터 노드 정보를 conf 파일에 저장합니다.
+- cluster-node-timeout : 노드간 통신이 되지 않아 timeout 되는 시간을 설정합니다.
++
`,r:{minutes:2.03,words:608},y:"a",t:"Scouter"}}],["/04-HashiCorp/07-Nomad/05-SampleJob/service-mesh-test.html",{loader:()=>u(()=>import("./service-mesh-test.html-ByTBwNOA.js"),__vite__mapDeps([180,1])),meta:{d:1632449108e3,g:["Nomad","Consul","Sample","Job","Service Mesh"],e:` +- 공식 Github : https://github.com/scouter-project/scouter
+- Scouter 다운로드 +
++
+- scouter collector와 host-agent 를 실행하는 job에서 버전정보를 기반으로 다운로드
+- host agent는
+system
타입으로 모든 노드에서 실행되도록 구성- tomcat 다운로드 +
++
+- 다운로드 받지않고 호스트에 미리 설치해 놓는 방식이 더 가벼워보임 --> 아마도 Packer, Terraform의 조합이면 가능
+- 다운로드 받게 구성하면 컨테이너처럼 Nomad가 배포할 때마다 다운받아서 구성 가능
+- Template - server.xml +
++
+- server.xml 기본 구성에서 port가 선언되는 자리를 java option에서 받을 수 있도록 변경
+- Template 구성도 가능하고 미리 구성한 xml을 다운로드 받게 하는것도 괜찮아 보임
+- Consul과 함께 구성된 경우 Nginx같은 LB 구성 Job 에서 backend를 동적으로 구성 가능
+- Nomad에 Scouter Collector와 Host Agent를 위한 별도 Namespace를 구성하는 것도 관리를 위해 좋아보임
+
+nomad namespace apply -description "scouter" scouter
HashiCorp 공식 Service Mesh Test App
+https://learn.hashicorp.com/tutorials/nomad/consul-service-mesh
+`,r:{minutes:.41,words:124},y:"a",t:"Service Mesh Test"}}],["/04-HashiCorp/07-Nomad/05-SampleJob/sidecar-tomcat.html",{loader:()=>u(()=>import("./sidecar-tomcat.html-CJjpWreP.js"),__vite__mapDeps([181,1])),meta:{d:1634806591e3,g:["Nomad","Sample","Job","sidecar","tomcat"],e:` +job "countdash" { + region = "global" + datacenters = ["dc1"] + # namespace = "mesh" + + group "api" { + network { + mode = "bridge" + port "api" { + to = 9001 # static 설정이 없으므로 컨테이너의 앱 9001과 노출되는 임의의 포트와 맵핑 + } + } + + service { + name = "count-api" + port = "api" # 임의의 포트 할당을 network port id로 지정 + + connect { + sidecar_service {} + } + } + + task "web" { + driver = "docker" + config { + image = "hashicorpnomad/counter-api:v1" + ports = ["api"] + } + } + } + + group "dashboard" { + network { + mode = "bridge" + port "http" { + static = 9002 # 컨테이너 앱 9002와 외부노출되는 사용자 지정 static port를 맵핑 + to = 9002 + } + } + + service { + name = "count-dashboard" + port = "http" # 할당된 포트를 network port id로 지정 + + connect { + sidecar_service { + proxy { + upstreams { + destination_name = "count-api" + local_bind_port = 8080 # backend인 count-api의 실제 port와는 별개로 frontend가 호출할 port 지정 + } + } + } + } + } + + task "dashboard" { + driver = "docker" + env { + COUNTING_SERVICE_URL = "http://${NOMAD_UPSTREAM_ADDR_count_api}" + } + config { + image = "hashicorpnomad/counter-dashboard:v1" + } + } + + scaling { + enabled = true + min = 1 + max = 10 + } + } +} +
+
`,r:{minutes:.27,words:82},y:"a",t:"tomcat-sidecar"}}],["/04-HashiCorp/07-Nomad/05-SampleJob/tomcat.html",{loader:()=>u(()=>import("./tomcat.html-DFo0Pri6.js"),__vite__mapDeps([182,1])),meta:{d:1632449108e3,g:["Nomad","Consul","Sample","Job"],e:` +- 참고 링크 + +
++
`,r:{minutes:2.07,words:620},y:"a",t:"Tomcat"}}],["/04-HashiCorp/07-Nomad/05-SampleJob/update.html",{loader:()=>u(()=>import("./update.html-B_F_Bdp6.js"),__vite__mapDeps([183,1])),meta:{d:163347429e4,g:["Nomad","Sample","Job"],e:` +- tomcat 다운로드 +
++
+- 다운로드 받지않고 호스트에 미리 설치해 놓는 방식이 더 가벼워보임 --> 아마도 Packer, Terraform의 조합이면 가능
+- 다운로드 받게 구성하면 컨테이너처럼 Nomad가 배포할 때마다 다운받아서 구성 가능
+- Template - server.xml +
++
+- server.xml 기본 구성에서 port가 선언되는 자리를 java option에서 받을 수 있도록 변경
+- Template 구성도 가능하고 미리 구성한 xml을 다운로드 받게 하는것도 괜찮아 보임
+- Consul과 함께 구성된 경우 Nginx같은 LB 구성 Job 에서 backend를 동적으로 구성 가능
+++팁
+nomad의 배포 방법 중 canary와 rolling update 관련된 내용입니다.
+`,r:{minutes:.24,words:72},y:"a",t:"update"}}],["/04-HashiCorp/07-Nomad/05-SampleJob/withConsulKV.html",{loader:()=>u(()=>import("./withConsulKV.html-vguQwm4X.js"),__vite__mapDeps([184,1])),meta:{d:1633322494e3,g:["Nomad","Sample","Job"],e:` +... + #canary update - 새로운 버전의 task를 canary 변수의 수만큼 기동시키고 상황에 맞게 확인 후 배포 + group "canary" { + count = 5 + + update { + max_parallel = 1 + canary = 1 + min_healthy_time = "30s" + healthy_deadline = "10m" + #배포 실패시 자동으로 전 버전으로 돌아감(배포 중이던 task 제거됨) + auto_revert = true + #task가 기동되어도 자동으로 다음 버전으로 넘어가지 않음(배포 전 버전 task 제거되지않음) + auto_promote = false + } + } + #rolling update - 기동 중이던 task를 하나씩(max_parallel만큼) 신규 task로 변환하면서 배포 + group "api-server" { + count = 6 + + update { + max_parallel = 2 + min_healthy_time = "30s" + healthy_deadline = "10m" + } + } + #배포 시 service에 canary로 배포된 task에만 붙일 수 있는 tag 설정 + service { + port = "http" + name = "canary-deployments" + + tags = [ + "live" + ] + + canary_tags = [ + "canary" + ] +} +... +
Consul의 KV에 값을 저장하고 비교하여 task batch를 수행하는 예제
++
`,r:{minutes:.54,words:162},y:"a",t:"Consul KV Sample"}}],["/04-HashiCorp/07-Nomad/05-SampleJob/withVaultKV.html",{loader:()=>u(()=>import("./withVaultKV.html-BnsB2ERk.js"),__vite__mapDeps([185,1])),meta:{d:1642495624e3,g:["Nomad","Sample","Job"],e:` +- curl 을 사용하는 경우
+curl -X GET http://127.0.0.1:8500/v1/kv/docmoa/commit_date | jq -r '.[0].Value | @base64d' +
- template의
+key
를 사용하는 경우{{ key "docmoa/commit_date" }} +
nomad job에서 vault의 secret에서 kv사용하기
++
+- Nomad 설정 링크
+nomad hcl 설정
+`,r:{minutes:.86,words:258},y:"a",t:"vaultKV"}}],["/04-HashiCorp/08-Updates/97-2024/2024-01.html",{loader:()=>u(()=>import("./2024-01.html-BqO2nuKk.js"),__vite__mapDeps([186,1])),meta:{d:1704238435e3,g:["Hashicorp","Update","Jan"],e:` +job "logs" { + datacenters = ["dc1"] + + constraint { + attribute = "${attr.kernel.name}" + value = "linux" + } + + update { + stagger = "10s" + max_parallel = 1 + } + + group "elk" { + count = 1 + + restart { + attempts = 2 + interval = "1m" + delay = "15s" + mode = "delay" + } + network { + port "elk" { + to = 9200 + static = 9200 + } + port "kibana" { + to = 5601 + } + port "logstash" { + to = 5000 + } + } + + task "elasticsearch" { + driver = "docker" + + vault { + policies = ["admin"] + change_mode = "signal" + change_signal = "SIGINT" + } + + config { + image = "elasticsearch:7.16.2" + ports = ["elk"] + volumes = [ + "local/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml", + ] + } + template { + data = <<EOF +cluster.name: "my-cluster" +network.host: 0.0.0.0 +discovery.type: single-node +discovery.seed_hosts: ["127.0.0.1"] +xpack.security.enabled: true +xpack.license.self_generated.type: trial +xpack.monitoring.collection.enabled: true +EOF + destination = "local/elasticsearch.yml" + change_mode = "signal" + change_signal = "SIGHUP" + } + template { + data = <<EOH +ELASTIC_PASSWORD="{{with secret "secret/elastic"}}{{.Data.passwd}}{{end}}" +EOH + + destination = "secrets/file.env" + env = true +} + + service { + name = "${TASKGROUP}-elasticsearch" + port = "elk" + } + + resources { + cpu = 500 + memory = 1048 + } + } + + task "kibana" { + driver = "docker" + + vault { + policies = ["admin"] + change_mode = "signal" + change_signal = "SIGINT" + } + + config { + image = "kibana:7.16.2" + ports = ["kibana"] + volumes = [ + "local/kibana.yml:/usr/share/kibana/config/kibana.yml" + ] + } + template { + data = <<EOF +# +# ** THIS IS AN AUTO-GENERATED FILE ** +# + +# Default Kibana configuration for docker target +server.host: "0.0.0.0" +server.shutdownTimeout: "5s" +elasticsearch.hosts: [ "http://{{ env "NOMAD_IP_elk" }}:{{ env "NOMAD_PORT_elk" }}" ] +elasticsearch.username: elastic +{{ with secret "secret/elastic" }} +elasticsearch.password: {{.Data.passwd}} +{{ end }} + +EOF + + destination = "local/kibana.yml" + change_mode = "signal" + change_signal = "SIGHUP" + } + + service { + name = "${TASKGROUP}-kibana" + port = "kibana" + check { + type = "http" + path = "/" + interval = "10s" + timeout = "2s" + } + } + + resources { + cpu = 500 + memory = 1200 + } + } + + task "logstash" { + driver = "docker" + + config { + image = "logstash:7.16.2" + ports = ["logstash"] + volumes = [ + "local/logstash.yml:/usr/share/logstash/config/logstash.yml" + ] + } + template { + data = <<EOF +http.host: "0.0.0.0" +xpack.monitoring.elasticsearch.hosts: [ "http://{{ env "NOMAD_IP_elk" }}:{{ env "NOMAD_PORT_elk" }}" ] +EOF + + destination = "local/logstash.yml" + change_mode = "signal" + change_signal = "SIGHUP" + } + + service { + name = "${TASKGROUP}-logstash" + port = "logstash" + } + + resources { + cpu = 200 + memory = 1024 + } + } + } +} +
Product 소개
++
`,r:{minutes:1.12,words:336},y:"a",t:"2024년 1월"}}],["/04-HashiCorp/08-Updates/97-2024/2024-02.html",{loader:()=>u(()=>import("./2024-02.html-C7mJntCJ.js"),__vite__mapDeps([187,1])),meta:{d:17072917e5,g:["Hashicorp","Update","Feb"],e:` +- HashiCorp 2023 year in review: Community +
++
+- Hashicorp Blog
+- 작년 2023년 한 해동안 있었던 Hashicorp 관련 이야기: 개최된 컨퍼런스 및 이벤트부터 새로 출시된 솔루션 별 트레이닝 및 자격증 관련, 그리고 창업자 Mitchell Hashimoto 의 퇴사 소식 등을 한 번에 확인하실 수 있습니다.
+Product 소개
++
`,r:{minutes:.75,words:226},y:"a",t:"2024년 2월"}}],["/04-HashiCorp/08-Updates/98-2023/2023-01.html",{loader:()=>u(()=>import("./2023-01.html-ggQMyTJT.js"),__vite__mapDeps([188,1])),meta:{d:1672798178e3,g:["Hashicorp","Update","Jan"],e:` +- HCP Vault Radar begins limited beta +
++
+- Hashicorp Blog
+- 작년 2023년 10월 Hashiconf 에서 공개된 HCP Vault Radar 가 Alpha 를 거쳐 Beta 가 출시되었습니다. Beta 에서는 RBAC/ABAC 을 지원하며 스캔할 수 있는 새로운 데이터 소스도 선보입니다
+Product 소개
++
`,r:{minutes:1.03,words:308},y:"a",t:"2023년 1월"}}],["/04-HashiCorp/08-Updates/98-2023/2023-02.html",{loader:()=>u(()=>import("./2023-02.html-ClDBGKm0.js"),__vite__mapDeps([189,1])),meta:{d:1675423442e3,g:["Hashicorp","Update","Feb"],e:` +- +
+Dynamic Secrets for Waypoint with Vault
++
+- Hashicorp Blog
+- Application 에 대한 Build, Deployment 및 Monitoring 을 간소화하고 쉽게 접근할 수 있도록 개발자를 지원하는 Waypoint 에서도 이제 Vault 와 연동하여 Hashicorp Vault config sourcer plugin 을 통해 Application Config 에 포함되는 보안 정보를 관리할 수 있습니다.
+Product 소개
++
`,r:{minutes:.95,words:285},y:"a",t:"2023년 2월"}}],["/04-HashiCorp/08-Updates/98-2023/2023-03.html",{loader:()=>u(()=>import("./2023-03.html-C303hGPW.js"),__vite__mapDeps([190,1])),meta:{d:167819845e4,g:["Hashicorp","Update","Mar"],e:` +- +
+Terraform Cloud Adds ‘Projects’ to Organize Workspaces at Scale
++
+- Hashicorp Blog
+- 기존의 Terraform Cloud 에서는 연관되는 Workspace 간 그룹화가 불가능하고 Organization 및 Workspace 단위로만 권한 할당이 가능함으로 인해 사용자들은 자원 활용에 대한 제약사항을 고려하여 Organization 및 Workspace 를 분할하는 불편함을 감수해야 했습니다. 'Projects' 는 Workspace 를 그룹화하고 Project 단위로 권한 설정을 지원함으로써 앞서 소개한 제약사항을 해소하도록 지원합니다.
+Product 소개
++
`,r:{minutes:1.23,words:370},y:"a",t:"2023년 3월"}}],["/04-HashiCorp/08-Updates/98-2023/2023-04.html",{loader:()=>u(()=>import("./2023-04.html-sQgt4Mf_.js"),__vite__mapDeps([191,1])),meta:{d:1680766775e3,g:["Hashicorp","Update","Apr"],e:` +- +
+Writing Terraform for unsupported resources
++
+- Hashicorp Blog
+- Terraform 과 대상 환경 간 연동을 위해 Provider 를 활용할 때 대상 환경이 API 을 통해서는 지원하는 기능이지만 Provider 에는 구현되어 있지 않아 사용할 수 없는 기능이 종종 있습니다 (예: Vault Provider 기반 Vault 운영 시 Unseal 기능 사용 불가). Terracurl 을 통해 API 을 통해서만 지원되는 기능들을 Terraform Code 로 구성하여 하나의 Resource 로 관리할 수 있습니다.
+Product 소개
++
`,r:{minutes:.96,words:287},y:"a",t:"2023년 4월"}}],["/04-HashiCorp/08-Updates/98-2023/2023-05.html",{loader:()=>u(()=>import("./2023-05.html-iXkPu4qP.js"),__vite__mapDeps([192,1])),meta:{d:1682489034e3,g:["Hashicorp","Update","May"],e:` +- +
+Dynamic provider credentials now generally available for Terraform Cloud
++
+- Hashicorp Blog
+- AWS, MS Azure, GCP 등 Cloud 환경을 Terraform 과 연동 및 인증하기 위해 Workspace Variable 또는 Variable Set 을 활용하여 Credential 정보를 설정해서 사용했습니다. 해당 Credential 정보는 장기간의 TTL 을 설정하여 사용하거나 혹은 보안취약점을 보완하기 위해 관리자가 수동으로 갱신 및 설정하는 등의 번거로움을 동반하고 있었습니다. Dynamic Provider Credential 은 각 Cloud Service 의 OIDC Provider 를 기반으로 Terraform 에 대한 TLS 인증 확인을 수행함으로써 매 Apply 마다 Terraform 에 대한 인증 처리 후 Resource Provisioning 등을 수행하는
+동적인증처리
를 지원합니다.- Hashicorp Officlal Tutorial 을 참고하여 테스트 해보실 수 있습니다.
+Product 소개
++
`,r:{minutes:.91,words:272},y:"a",t:"2023년 5월"}}],["/04-HashiCorp/08-Updates/98-2023/2023-06.html",{loader:()=>u(()=>import("./2023-06.html-BjObLeBp.js"),__vite__mapDeps([193,1])),meta:{d:1686116077e3,g:["Hashicorp","Update","Jun"],e:` +- +
+Vault Secrets Operator: A new method for Kubernetes integration
++
+- Hashicorp Blog
+- K8S Cluster 의 Secret 을 CRD (custom resource definitions) 를 기반으로 Vault 와 연동함으로써 K8S 를 이용하는 개발자 및 다양한 사용자들은 새로운 Tool 을 배울 필요 없이 기존의 경험을 바탕으로 K8S Secret 을 생명주기가 더해진 동적인 데이터로써 사용 가능합니다 .
+- 기존에 제공되고 있던 Sidecar Injector 및 CSI Provider 방식과의 비교 분석은 Hashicorp Blog: Kubernetes Vault Integration via Sidecar Agent Injector vs. Vault Secrets Operator vs. CSI Provider 를 참고하세요.
+- Hashicorp Officlal Tutorial 을 참고하여 테스트 해보실 수 있습니다.
+Product 소개
++
`,r:{minutes:1.29,words:388},y:"a",t:"2023년 6월"}}],["/04-HashiCorp/08-Updates/98-2023/2023-07.html",{loader:()=>u(()=>import("./2023-07.html-CSUKPa-P.js"),__vite__mapDeps([194,1])),meta:{d:1689171954e3,g:["Hashicorp","Update","Jul"],e:` +- +
+Terraform Cloud updates plans with an enhanced Free tier and more flexibility
++
+- Hashicorp Blog
+- Terraform Cloud 에 대한 요금제가 개편됩니다. 요금제 개편과 함께 각 요금제에서 사용가능한 기능들도 추가되었습니다. (예를 들어 기존 Free Tier 에서는 사용불가 했던 Sentinel/OPA, SSO, Terraform Agent, Run Tasks 등)
+- +
+Terraform Cloud adds Vault-backed dynamic credentials
++
+- Hashicorp Blog
+- 지난 4월 소개된 Dynamic provider credentials now generally available for Terraform Cloud 에 이어 Vault의 Cloud Secrets Engine (AWS, Azure, GCP) 를 연계한 동적 자격증명 발급 기능이 출시되었습니다. 이제, Terraform Apply 수행 시 마다 Vault 로 부터 자격증명을 발급받아 사용함으로써 보다 보안 강화된 워크플로우를 구성할 수 있습니다.
+Product 소개 (Hashidays 2023)
++
`,r:{minutes:1.49,words:448},y:"a",t:"2023년 7월"}}],["/04-HashiCorp/08-Updates/98-2023/2023-08.html",{loader:()=>u(()=>import("./2023-08.html-Bq8h3G2V.js"),__vite__mapDeps([195,1])),meta:{d:1690862751e3,g:["Hashicorp","Update","Aug"],e:` +- 매년 유럽 네덜란드에서 이틀간 진행되던 Hashiconf 가 올해는 한단계 더 성장하여 영국 런던, 프랑스 파리 그리고 독일 뮌헨에서 동시에 진행되는 Hashidays 2023 으로 개최되었습니다. 새롭게 펼쳐진 Hashidays 에서는 Terraform, Vault, Consul 그리고 Boundary 에 대해 그동안 사용자들이 사용하면서 느꼈던 불편함을 해소할 신기능을 출시했습니다. +
++
+- Hashicorp Blog
+- HCP Vault Secrets (Public Beta): Vault 에서 가장 많이 활용되고 있는 Secret Engine 중 하나인 KV Engine 기반 시크릿 관리에 특화된 HCP SaaS 서비스로써 애플리케이션 개발 및 운영에 사용되는 시크릿에 대한 저장, 접근 그리고 자동 동기화 기능을 더욱 쉽게 구성 가능하도록 지원합니다. 특히 기존 사용 중인 AWS Secret Manager 등 클라우드 서비스와도 원클릭 동기화를 지원함으로써 기존 워크플로우에 대해 최소한의 변경으로 시크릿 동적관리가 가능합니다.
+- Vault Secrets Operator: CRD 기반으로 K8S의 Secrets 와 연계하여 Secrets 를 동적으로 관리함으로써 최소한의 추가 구성으로 기존 구성하여 사용중인 Secrets 에 대해 보안을 강화합니다.
+- Boundary Enterprise: 기존에 Opensource 그리고 HCP SaaS 로만 제공되던 Boundary 가 드디어 Enterprise Edition 이 출시되었습니다. Enterprise Edition 에서는 그동안 많은 사용자들이 필요로 했던 Session Recording 과 더불어 그외 다양한 신규 기능들이 추가되었습니다.
+- Terraform: 지난 6월호에서 소개한 Vault-backed dynamic credentials 의 정식 GA 출시, 그동안 너무 불편하고 어려웠던 Terraform import 를 보완해줄 Config-driven Import, 생성 및 관리 중인 자원에 대한 보다 효과적인 관리를 위한 Explorer (Beta) 등 다양한 기능이 추가되었습니다.
+- Consul 1.16: Envoy Proxy 에 Extension 이 출시되어 WASM (Web Assembly) Code 기반 추가 기능을 활용하거나 외부 보안 서비스와 연계하여 인증 기반 기능 활용합니다, 또한 여러 Cluster 에 걸쳐 동일 서비스에 대해 동일 서비스 이름을 사용하도록 하는 Sameness Groups 를 통해 서비스 관리 뿐만 아니라 장애 발생 시 수행하는 Failover 를 보다 간단하게 처리할 수 있습니다.
+Product 소개
++
`,r:{minutes:1.01,words:302},y:"a",t:"2023년 8월"}}],["/04-HashiCorp/08-Updates/98-2023/2023-09.html",{loader:()=>u(()=>import("./2023-09.html-D8Ie08jE.js"),__vite__mapDeps([196,1])),meta:{d:1693797134e3,g:["Hashicorp","Update","Sep"],e:` +- Using Terraform dynamic provider credentials in your AWS landing zones +
++
+- Hashicorp Blog
+- 지난 4월 소개된 Terraform 사용 시 필요한 대상 환경에 대한 클라우드 자격증명을 Vault 와 연동하여 동적으로 사용 및 관리하는 Dynamic Provider Credentials 기능을 AWS Landing Zone 에서도 사용하실 수 있습니다. Terraform 과 함께 Landing Zone 으로 시작하는 AWS 의 여정에서 더 보안 강화된 IaC 를 경험해보세요!
+Product 소개
++
`,r:{minutes:.95,words:284},y:"a",t:"2023년 9월"}}],["/04-HashiCorp/08-Updates/98-2023/2023-10.html",{loader:()=>u(()=>import("./2023-10.html-DCZIi768.js"),__vite__mapDeps([197,1])),meta:{d:1696476441e3,g:["Hashicorp","Update","Oct"],e:` +- Terraform ephemeral workspaces public beta now available +
++
+- Hashicorp Blog
+- 개발 및 테스트 등을 위해 임시로 사용하는 Workspace 에 대해 사용 완료 후 방치함으로 인해 발생되는 자원 낭비 또는 보안 유출 위험성을 방지하고자 Workspace 사용에 대해 미리 시간 설정을 할 수 있는 기능이 베타로 출시되었습니다. 이제, 정해놓은 타이머가 도래하면 Workspace 와 해당 Workspace 를 통해 생성한 자원을 자동 폐기 및 정리함으로써 자원 관리의 효율성을 높이고 미사용 자원에 대한 보안 유출 등을 방지할 수 있습니다. 베타 버전은 Terraform Cloud (Plus 요금제 이상) 에서 체험 및 사용 가능합니다.
+Product 소개
++
`,r:{minutes:.9,words:271},y:"a",t:"2023년 10월"}}],["/04-HashiCorp/08-Updates/98-2023/2023-11.html",{loader:()=>u(()=>import("./2023-11.html-CTNmw6DG.js"),__vite__mapDeps([198,1])),meta:{d:1698990531e3,g:["Hashicorp","Update","Nov"],e:` +- Creating a multi-cloud golden image pipeline with Terraform Cloud and HCP Packer +
++
+- Hashicorp Blog
+- 조직 내 클라우드 사용환경에서 "표준화" 되지 않은 VM OS Image 로 인해 장애 발생 시 케이스를 표준화 하지 못하고 대응에 미진한 경우를 종종 접하곤 합니다. Hashicorp Packer 와 Terraform 을 연동하여 조직 내 사용중인 각 클라우드 환경 마다 Golden OS Image 를 구성하고 이를 활용하여 인스턴스 자원 배포하는 과정까지의 라이프사이클을 "표준화" 함으로써 보다 더 효율적이고 안정적인 시스템 환경을 구성해보세요.
+Product 소개
++
`,r:{minutes:.99,words:298},y:"a",t:"2023년 11월"}}],["/04-HashiCorp/08-Updates/99-2022/2022-07.html",{loader:()=>u(()=>import("./2022-07.html-DG6lpmIo.js"),__vite__mapDeps([199,1])),meta:{d:1656980314e3,g:["Hashicorp","Update","July"],e:` +- Infrastructure and security releases open HashiConf 2023 +
++
+- Hashicorp Blog
+- 샌프란시스코에서 개최된 Hashiconf 2023 에서 8가지의 솔루션에 대해 그룹을 크게 Infrastructure 와 Security 로 구분지어 앞으로의 솔루션 포트폴리오 및 업데이트를 진행하며, Terraform test framework, Vault Secret Sync, Vault Radar 등 워크플로우 개선을 위한 새로운 기능이 공개했습니다. 자세한 사항은 행사 현장을 직접 다녀온 이들이 전해주는 Hashicorp Korea: Hashiconf 2023 에서 확인하세요!
+Product 소개
++
`,r:{minutes:1.27,words:382},y:"a",t:"2022년 7월"}}],["/04-HashiCorp/08-Updates/99-2022/2022-08.html",{loader:()=>u(()=>import("./2022-08.html-4-PzckqW.js"),__vite__mapDeps([200,1])),meta:{d:1659510375e3,g:["Hashicorp","Update","Aug"],e:` +- +
+HCP Boundary 출시 (Public Beta)
++
+- HCP Boundary 소개 Blog
+- Hashicorp Korea Snapshot
+- HCP Boundary 시작하기
+- Hashicorp 는 모든 솔루션에 대해 사용자가 직접 설치하는 설치형 을 비롯해 이와 동일한 경험을 기반으로 SaaS 형태의 Cloud 서비스를 제공하고 있습니다. 지난 6월 21일, HCP Boundary 의 Public Beta 가 공개되어 무료 제공 중입니다.
+- AWS Platform 에 One click 으로 Cluster 생성 및 이용 가능하며 간단한 Network Peering 과정을 거쳐 AWS Platform 에 구성된 HVN (Hashicorp Virtual Network) 및 Cluster 와 사용자의 AWS 환경을 연결하여 미리 구성한 서비스들을 연동합니다. (AWS 지원 Region 확장 및 타 Cloud Platform 지원 예정)
+- HCP 계정 생성 시, USD 50불이 기본 Credit 으로 제공되며 이를 활용하여 Boundary Public Beta 외에도 다양한 Vault, Consul 과 같은 HCP Service 을 약 1개월간 체험해보실 수 있습니다.
+- +
+Hashicorp Developer Site 출시 (Public Beta)
++
+- Hashicorp Developer Site 소개 Blog
+- Hashicorp Developer Site
+- Tutorial 과 Reference Architecture 정보가 learn.hashicorp.com 을 비롯, 각 solution 별 website 에 파편화 되어 있어 Hashicorp Solution 을 보다 더 쉽게 이해하고 업무에 적용하는데에 어려움이 있었습니다. 새롭게 출시된 Hashicorp Developer Site 에서는 이러한 그동안 축적된 유용한 자료와 이를 테스트 해볼 수 있는 환경을 한 곳에 모아 통합 제공함으로써 보다 더 쉽게 Hashicorp Solution 을 경험할 수 있습니다.
+- Public Beta 기간에는 Hashicorp Solution 중 Vault 와 Waypoint 에 대해 이용 가능하고, 추후 모든 Solution 에 대해 제공 예정입니다.
+Product 소개
++
`,r:{minutes:.69,words:207},y:"a",t:"2022년 8월"}}],["/04-HashiCorp/08-Updates/99-2022/2022-09.html",{loader:()=>u(()=>import("./2022-09.html-DEZ_nh_3.js"),__vite__mapDeps([201,1])),meta:{d:1662203164e3,g:["Hashicorp","Update","Sep"],e:` +- +
+Consul Service Mesh 에 대한 AWS Lambda 지원 (Public Beta)
++
+- Hashicorp Blog
+- Service Mesh 내 구성된 Service 가 AWS Lambda 를 호출 할 수 있도록 지원함으로써, 기존의 K8S, VM, Nomad 혹은 Amazon ECS 등의 다양한 환경과 더불어 Serverless 환경까지 통합 지원하여 Service Mesh 구성의 범위를 확장하고 Workflow 일원화가 가능합니다.
+- 참고문서 1: Register Lambda Functions
+- 참고문서 2: Invoke Lambda Fuctions
+- 참고문서 3: Terraform Registry: consul-lambda-registrator
+Product 소개
++
`,r:{minutes:.83,words:249},y:"a",t:"2022년 9월"}}],["/04-HashiCorp/08-Updates/99-2022/2022-10.html",{loader:()=>u(()=>import("./2022-10.html-y6mQjU3f.js"),__vite__mapDeps([202,1])),meta:{d:1664864834e3,g:["Hashicorp","Update","Oct"],e:` +- +
+CDKTF (Cloud Development Kit for Terraform) General Available
++
+- Hashicorp Blog
+- Python, Go 등 프로그래밍 언어 기반으로 Terraform 을 활용하실 수 있도록 지원하는 CDKTF 가 정식 출시 되었습니다. CDKTF를 사용하면 개발자는 익숙한 프로그래밍 언어에서 컨텍스트 전환 없이 코드로 인프라를 설정할 수 있으며, 애플리케이션 비즈니스 로직을 정의하는 데 사용하는 인프라 리소스를 프로비저닝하기 위해 동일한 도구와 구문을 사용할 수 있습니다. 팀은 익숙한 구문으로 협업하면서 Terraform 에코시스템의 기능을 계속 활용하고 확립된 Terraform 배포 파이프라인을 통해 인프라 구성을 배포할 수 있습니다.
+- 참고문서 1: CDK for Terraform v0.12: CHANGELOG
+- 참고문서 2: CDKTF Overview
+- 참고문서 3: CDKTF Tutorials
+Product 소개
++
`,r:{minutes:.75,words:225},y:"a",t:"2022년 10월"}}],["/04-HashiCorp/08-Updates/99-2022/2022-11.html",{loader:()=>u(()=>import("./2022-11.html-m34nQJRW.js"),__vite__mapDeps([203,1])),meta:{d:166729331e4,g:["Hashicorp","Update","Nov"],e:` +- +
+Nomad: Nomad Variables and Service Discovery
++
+- Hashicorp Blog
+- Hashicorp Nomad는 Container 뿐만 아니라 Container 화 하기 어려운 Legacy Application 에 대해 배포하고 관리하는 데 사용되는 간단하고 유연한 오케스트레이터입니다. Nomad는 On-prem 및 Cloud 환경을 가리지 않고 작동합니다. Cloudflare, Roblox, Q2 및 Pandora와 같은 조직의 프로덕션에서 널리 채택되고 사용됩니다. 새롭게 출시된 HashiCorp Nomad 1.4 Beta Release 에서는 상태 확인을 통해 Service Discovery 지원을 강화하고 사용자가 구성 값을 저장할 수 있도록 하는 Nomad Variable 기능이 도입 되었습니다.
+Product 소개
++
`,r:{minutes:1.7,words:511},y:"a",t:"2022년 11월"}}],["/04-HashiCorp/08-Updates/99-2022/2022-12.html",{loader:()=>u(()=>import("./2022-12.html-C2Kpn3zh.js"),__vite__mapDeps([204,1])),meta:{d:1670381763e3,g:["Hashicorp","Update","Dec"],e:` +- +
+Hashiconf Global
++
+- + +
+- +
+Day 1: ZTS (Zero Trust Security) 와 Cloud Service Networking 을 메인 주제로 새로운 기능과 HCP 서비스에 대한 소개
++
+- HCP Boundary GA: Opensource 버전만 지원하던 Boundary 에 대해 HCP Version 공식 출시
+- HCP Vault on Microsoft Azure Public Beta: AWS 뿐만 아니라 Azure 사용자들도 HCP Vault 사용 가능
+- Consul Breaking Changes: K8S 에서의 Component 간소화 및 기능강화, Windows 에 대한 Service Mesh 지원
+- Hashicorp Developer Portal 개편: 8가지 전 제품군에 대한 공식 문서 및 Tutorial 총망라
+- +
+Day 2: Infrastructure 및 Application 자동화 관련 제품군을 메인 주제로 새로운 기능 소개
++
+- Terraform Cloud: Self Service 및 Compliance 관련 기능 강화
+- Nomad 1.4 GA: 단독 제품으로서의 Service Discovery, Variable 등 기능 강화
+- HCP Waypoint Public Beta 출시: Opensource 버전만 지원하던 Waypoint 의 HCP Version Public Beta 출시
+Product 소개
++
`,r:{minutes:.82,words:247},y:"a",t:"2022년 12월"}}],["/05-Software/Jenkins/pipeline101/00-introduction.html",{loader:()=>u(()=>import("./00-introduction.html-BtI6zsW3.js"),__vite__mapDeps([205,1])),meta:{d:164032788e4,g:["cicd","jenkins"],e:` +- +
+Terraform Run Tasks in Public Registry
++
+- Hashicorp Blog
+- 활발하게 활용되고 있는 Terraform Run Tasks (3rd party 연동 및 통합) 기능이 강화되었습니다. 이제 Terraform Public Registry 에서 Run Tasks 를 검색하여 필요한 3rd party service 와 연동하여 자원 관리에 필요한 Cost Management, Policy Compliance 와 같은 다양한 기능들을 적용하여 보다 효율적인 자원 관리가 가능합니다.
+++Update at 31 Jul, 2019
+Jenkins Pipeline 을 구성하기 위해 VM 환경에서 Jenkins와 관련 Echo System을 구성합니다. 각 Product의 버전은 문서를 작성하는 시점에서의 최신 버전을 위주로 다운로드 및 설치되었습니다. 구성 기반 환경 및 버전은 필요에 따라 변경 가능합니다.
++ +
`,r:{minutes:.96,words:287},y:"a",t:"Pipeline on Jenkins 101 : Introduction"}}],["/05-Software/Jenkins/pipeline101/01-cicd.html",{loader:()=>u(()=>import("./01-cicd.html-OsUvvkt4.js"),__vite__mapDeps([206,1])),meta:{d:164032788e4,g:["cicd","jenkins"],e:` ++ + + +Category +Name +Version ++ +VM +VirtualBox +6.0.10 ++ +OS +Red Hat Enterprise Linux +8.0.0 ++ +JDK +Red Hat OpenJDK +1.8.222 ++ + +Jenkins +Jenkins rpm +2.176.2 +1.1 CI/CD Concept Definitions
++
+- Continuous integration
+- Continuous delivery
+- Continuous deployment
+- Source control management (SCM)
+1.2 Delivery vs Deployment
++
`,r:{minutes:.23,words:70},y:"a",t:"1. CI/CD"}}],["/05-Software/Jenkins/pipeline101/02-jobs.html",{loader:()=>u(()=>import("./02-jobs.html-Cq6dIiBF.js"),__vite__mapDeps([207,208,1])),meta:{d:164032788e4,g:["cicd","jenkins"],e:` +- Continuous Delivery requires user intervention +
++
+- When? : Stage to Production
+프로젝트는 Job의 일부 입니다. 즉, 모든 프로젝트가 Job이지만 모든 Job이 프로젝트는 아닙니다. Job의 구조는 다음과 같습니다.
+FreeStyleProejct, MatrixProject, ExternalJob만
+New job
에 표시됩니다.2.1 New pipeline
+Step 1에서는
+stage
없이 기본 Pipeline을 실행하여 수행 테스트를 합니다.+
`,r:{minutes:1.83,words:548},y:"a",t:"2. Jobs"}}],["/05-Software/Jenkins/pipeline101/03-builds.html",{loader:()=>u(()=>import("./03-builds.html-sZm3VZd3.js"),__vite__mapDeps([209,210,1])),meta:{d:164032788e4,g:["cicd","jenkins"],e:` +- +
+Jenkins 로그인
+- +
+좌측
+새로운 Item
클릭- +
++
Enter an item name
에 Job 이름 설정 (e.g. 2.Jobs)- +
++
Pipeline
선택 후OK
버튼 클릭- +
++
Pipeline
항목 오른 쪽Try sample Pipelie...
클릭하여Hello world
클릭 후 저장node { + echo 'Hello World' +} +
- +
+좌측
+Build now
클릭- +
+좌측
+Build History
의 최근 빌드된 항목(e.g. #1) 우측에 마우스를 가져가면 dropdown 버튼이 생깁니다. 해당 버튼을 클릭하여Console Output
클릭- +
+수행된
+echo
동작 출력을 확인합니다.Started by user GyuSeok.Lee +Running in Durability level: MAX_SURVIVABILITY +[Pipeline] Start of Pipeline +[Pipeline] node +Running on Jenkins in /var/lib/jenkins/workspace/2.Jobs +[Pipeline] { +[Pipeline] echo +Hello World +[Pipeline] } +[Pipeline] // node +[Pipeline] End of Pipeline +Finished: SUCCESS +
3.1 Tracking build state
+Pipeline이 수행되는 동작을 추적하는 과정을 확인합니다. 이를 이를 위한 Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 03-01.TrackingBuildState)
+Pipeline에 다음과 같이 스크립트를 추가합니다.
+`,r:{minutes:1.62,words:487},y:"a",t:"3. Builds"}}],["/05-Software/Jenkins/pipeline101/04-agent.html",{loader:()=>u(()=>import("./04-agent.html-DwW982Yy.js"),__vite__mapDeps([211,212,1])),meta:{d:164032788e4,g:["cicd","jenkins"],e:` +pipeline { + agent any + stages { + stage('Deploy') { + steps { + timeout(time: 1, unit: 'MINUTES') { + sh 'for n in \`seq 1 10\`; do echo $n; sleep 1; done' + } + timeout(time: 1, unit: 'MINUTES') { + sh 'for n in \`seq 1 50\`; do echo $n; sleep 1; done' + } + } + } + } +} +
빌드를 수행하기 위한 Worker로 다중 Jenkins를 컨트롤 할 수 있습니다. 이때 명령을 수행하는 Jenkins는
+Master
, 빌드를 수행하는 Jenkins는Worker
로 구분합니다. 여기서는 Worker의 연결을 원격 호스트의 Jenkins를 SSH를 통해 연결하는 방식과 컨테이너로 구성된 Jenkins를 연결하는 과정을 확인 합니다.Master-Slave 방식, 또는 Master-Agent 방식으로 표현합니다.
++`,r:{minutes:2.31,words:693},y:"a",t:"4. Agents and Distributing Builds"}}],["/05-Software/Jenkins/pipeline101/05-plugins.html",{loader:()=>u(()=>import("./05-plugins.html-C_L6Paff.js"),__vite__mapDeps([213,214,1])),meta:{d:164032788e4,g:["cicd","jenkins"],e:` +팁
+※ Slave 호스트에 Jenkins를 설치할 필요는 없습니다.
+Jenkins가 유용한 툴인 이유중 하나는 방대한 양의 플러그인 입니다. Jenkins의 기능을 확장시키고, 관리, 빌드 정책 등을 확장 시켜주고, 타 서비스와의 연계를 쉽게 가능하도록 합니다.
+ + +5.1 Adding plugins via plugin manager
`,r:{minutes:.65,words:196},y:"a",t:"5. Plugins"}}],["/05-Software/Jenkins/pipeline101/06-notifications.html",{loader:()=>u(()=>import("./06-notifications.html-CawbIy-9.js"),__vite__mapDeps([215,216,1])),meta:{d:164032788e4,g:["cicd","jenkins"],e:` +Jenkins빌드의 결과를 받아볼 수 있는 몇가지 방안에 대해 알아봅니다.
+6.1 Notifications of build state
+Jenkins에서는 플러그인이나 외부 툴에 의해 빌드에 대한 결과를 받아 볼 수 있습니다. 대표적으로는 Jenkins의 슬랙 플러그인을 사용하여 슬랙으로 빌드에 결과를 받아보거나, catlight.io 에서 데스크탑용 어플리케이션에 연동하는 방법도 있습니다.
`,r:{minutes:.62,words:187},y:"a",t:"6. Notifications"}}],["/05-Software/Jenkins/pipeline101/07-testing.html",{loader:()=>u(()=>import("./07-testing.html-DItX3hY7.js"),__vite__mapDeps([217,218,1])),meta:{d:164032788e4,g:["cicd","jenkins"],e:` +7.1 Code coverage tests and reports
+테스트 Pipeline 구성시 테스트 과정을 지정할 수 있습니다. Testing을 위한
+Pipeline
타입의 Item을 추가로 생성합니다. (e.g. 07-01.CodeCoverageTestsAndReports)설정은 다음과 같이 수행합니다.
++
`,r:{minutes:.68,words:204},y:"a",t:"7. Testing"}}],["/05-Software/Jenkins/pipeline101/08-restapi.html",{loader:()=>u(()=>import("./08-restapi.html-E0A3Day-.js"),__vite__mapDeps([219,1])),meta:{d:164032788e4,g:["cicd","jenkins"],e:` +- +
++
Pipeline
스크립트에 다음과 같이 입력 합니다. 테스트와 빌드, 검증 후 결과를 보관하는 단계까지 이루어 집니다.pipeline { + agent any + stages { + stage('Build') { + steps { + sh ''' + echo This > app.sh + echo That >> app.sh + ''' + } + } + stage('Test') { + steps { + sh ''' + grep This app.sh >> \${BUILD_ID}.cov + grep That app.sh >> \${BUILD_ID}.cov + ''' + } + } + stage('Coverage'){ + steps { + sh ''' + app_lines=\`cat app.sh | wc -l\` + cov_lines=\`cat \${BUILD_ID}.cov | wc -l\` + echo The app has \`expr $app_lines - $cov_lines\` lines uncovered > \${BUILD_ID}.rpt + cat \${BUILD_ID}.rpt + ''' + archiveArtifacts "\${env.BUILD_ID}.rpt" + } + } + } +} +
- +
+빌드가 완료되면 해당 Job화면을 리로드 합니다. Pipeline에
+archiveArtifacts
가 추가되었으므로 해당 Job에서 이를 관리합니다.
+- +
+해당 아카이브에는 코드 검증 후의 결과가 저장 됩니다.
+Jenkins는 외부 서비스와의 연동이나 정보 조회를 위한 API를 제공합니다.
+8.1 Triggering builds via the REST API
+Jenkins REST API 테스트를 위해서는 Jenkins에 인증 가능한 Token을 취득하고 curl이나 Postman 같은 도구를 사용하여 확인 가능 합니다. 우선 Token을 얻는 방법은 다음과 같습니다.
++
`,r:{minutes:.55,words:166},y:"a",t:"8. REST API"}}],["/05-Software/Jenkins/pipeline101/09-security.html",{loader:()=>u(()=>import("./09-security.html-CSGyCK8z.js"),__vite__mapDeps([220,221,1])),meta:{d:164032788e4,g:["cicd","jenkins"],e:` +- +
+Jenkins에 로그인 합니다.
+- +
+우측 상단의 로그인 아이디에 마우스를 호버하면 드롭박스 버튼이 나타납니다.
+설정
을 클릭합니다.- +
++
API Token
에서Current token
을 확인합니다. 등록된 Token이 없는 경우 다음과 같이 신규 Token을 발급 받습니다.+
+- +
++
ADD NEW TOKEN
을 클릭합니다.- +
+이름을 기입하는 칸에 로그인 한 아이디를 등록합니다. (e.g. admin)
+- +
++
GENERATE
를 클릭하여 Token을 생성합니다.- +
+이름과 Token을 사용하여 다음과 같이 curl로 접속하면
+Jenkins-Crumb
프롬프트가 나타납니다.$ curl --user "admin:TOKEN" 'http://myjenkins.com/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,":",//crumb)' + +Jenkins-Crumb:89e1fd9c402824c89465f6b97f49b605 +
- +
++
Crumb
를 확인했으면 다시 헤더 값에Jenkins-Crumb:
를 추가하여02-04.MultiStep
Job을 빌드하기 위해 다음과 같이 요청합니다.$ curl -X POST http://myjenkins.com/job/02-04.MultiStep/build --user gyulee:11479bdec9cada082d189938a3946348be --data-urlencode json='' -H "Jenkins-Crumb:89e1fd9c402824c89465f6b97f49b605" +
9.1 Securing your deployment with users
+사용자별 배포수행을 위한 사용자 설정을 설명합니다.
++
+- +
Jenkins 관리
로 이동하여Configure Global Security
를 클릭합니다.`,r:{minutes:1.44,words:433},y:"a",t:"9. Security"}}],["/05-Software/Jenkins/pipeline101/10-artifacts.html",{loader:()=>u(()=>import("./10-artifacts.html-Bs2kQ5kz.js"),__vite__mapDeps([222,223,1])),meta:{d:164032788e4,g:["cicd","jenkins"],e:` +
Enable security
는 보안 설정 여부를 설정하는 항목으로 기본적으로는 비활성화되어있습니다. 체크하여 활성화하면 다양한 보안 옵션을 설정할 수 있는 항목이 표기 됩니다.빌드 이후 빌드의 결과를 기록하고 저장하는 방법을 설명합니다.
+10.1 Creating and storing artifacts
+Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 10-01.CreatingAndStoringArtifacts)
+Pipeline에 다음과 같이 스크립트를 추가합니다.
+`,r:{minutes:.57,words:170},y:"a",t:"10. Artifacts"}}],["/05-Software/Jenkins/pipeline101/11-pipelines.html",{loader:()=>u(()=>import("./11-pipelines.html-C2Sm7x5b.js"),__vite__mapDeps([224,225,1])),meta:{d:164032788e4,g:["cicd","jenkins"],e:` +pipeline { + agent any + stages{ + stage('Build') { + steps{ + sh 'echo "Generating artifacts for \${BUILD_NUMBER}" > output.txt' + } + } + stage('Archive') { + steps { + archiveArtifacts artifacts: 'output.txt', onlyIfSuccessful: true + } + } + } +} +
11.1 Automating deployment with pipelines
+Pipeline 타입의 Item을 추가로 생성합니다. (e.g. 11-01.AutomatingDeploymentWithPipelines)
+Pipeline에 다음과 같은 스크립트를 입력합니다.
+`,r:{minutes:1.43,words:428},y:"a",t:"11. Pipelines"}}],["/05-Software/Jenkins/pipeline101/12-appendix.html",{loader:()=>u(()=>import("./12-appendix.html-DYgAE8cJ.js"),__vite__mapDeps([226,1])),meta:{d:164032788e4,g:["cicd","jenkins"],e:` +pipeline { + agent any + stages { + stage('Build') { + steps { + sh 'echo "Hello World"' + } + } + stage('Test') { + steps { + sh 'echo "Test Hello World!"' + } + } + } +} +
GitHub SCM 연동 이슈
+GitHub를 SCM으로 사용하는 경우 다음과 같은 메시지가 출력되면서 진행되지 않는 경우가 있습니다.
+`,r:{minutes:.31,words:93},y:"a",t:"Apendix"}}],["/05-Software/Jenkins/pipeline101/13-jenkins_101_single.html",{loader:()=>u(()=>import("./13-jenkins_101_single.html-Dgqr7ZGI.js"),__vite__mapDeps([227,208,210,212,214,216,218,221,223,225,1])),meta:{d:1640328154e3,g:["cicd","jenkins"],e:` +GitHub API Usage: Current quota has 5000 remaining (447380 over budget). Next quota of 5000 in 5 days 0 hr. Sleeping for 4 days 23 hr. +14:07:33 GitHub API Usage: The quota may have been refreshed earlier than expected, rechecking... +
++Update at 31 Jul, 2019
+Introduction
+Jenkins Pipeline 을 구성하기 위해 VM 환경에서 Jenkins와 관련 Echo System을 구성합니다. 각 Product의 버전은 문서를 작성하는 시점에서의 최신 버전을 위주로 다운로드 및 설치되었습니다. 구성 기반 환경 및 버전은 필요에 따라 변경 가능합니다.
++ +
`,r:{minutes:13.08,words:3923},y:"a",t:"Pipeline on Jenkins 101 (Single Page)"}}],["/05-Software/Tomcat/tomcat101/01-Introduction.html",{loader:()=>u(()=>import("./01-Introduction.html-DUosUNA9.js"),__vite__mapDeps([228,1])),meta:{d:164032788e4,g:["Tomcat","Java"],e:` ++ + + +Category +Name +Version ++ +VM +VirtualBox +6.0.10 ++ +OS +Red Hat Enterprise Linux +8.0.0 ++ +JDK +Red Hat OpenJDK +1.8.222 ++ + +Jenkins +Jenkins rpm +2.176.2 +본 내용은 톰캣을 좀더 잘 알고 잘 써보기 위한 제안이랄까요?
+톰캣의 특성상 쉽게 접할 수 있는 메뉴얼적인 지식보다는, 톰캣을 더 잘 사용하고 운영 할 수 있을만한 아이디어를 공유하고자 시작한 지식공유 활동입니다. 담고 있는 내용은 '톰캣 알고 쓰기' 유튜브 강의 내용에 대한 정리입니다. 유튜브에 강의를 올리면 출퇴근 시간을 이용해 짬짬히 들을 수 있을 것 같은 생각이 들어 시작하였지만
`,r:{minutes:1.32,words:396},y:"a",t:"1. Tomcat 소개"}}],["/05-Software/Tomcat/tomcat101/02-env.html",{loader:()=>u(()=>import("./02-env.html-DG_OSaqw.js"),__vite__mapDeps([229,1])),meta:{d:164032788e4,g:["Tomcat","Java"],e:` + +얼마나 출퇴근 시간에 이용하셨을지는 미지수이고동영상으로 모든 것을 다 표현할 수 없다는 점을 감안하여 다시 글로 정리합니다.
+2.1 OS
+톰캣을 설치하는 OS 플랫폼 환경은 모든 환경을 지원합니다. 그나마 예전에는 일부 Unix/Linux/OSX 환경에서 Apache HTTP Server 설치하듯 컴파일을 통해 구성하였으나, 최근에는 압축파일을 해제하고 바로 사용할 수 있는 경우가 대부분입니다.
`,r:{minutes:1.25,words:375},y:"a",t:"2. Tomcat 설치환경"}}],["/05-Software/Tomcat/tomcat101/03-installation.html",{loader:()=>u(()=>import("./03-installation.html-C2Y8sNUT.js"),__vite__mapDeps([230,1])),meta:{d:164032788e4,g:["Tomcat","Java"],e:` +
+톰캣을 운영하기 위해 OS를 선택해야하는 입장이라면 다음과 같은 설치 타입을 고려할 수 있습니다.+
+`,r:{minutes:1.38,words:414},y:"a",t:"3. Tomcat 설치"}}],["/05-Software/Tomcat/tomcat101/04-configuration.html",{loader:()=>u(()=>import("./04-configuration.html-DzqaMvOy.js"),__vite__mapDeps([231,1])),meta:{d:164032788e4,g:["Tomcat","Java"],e:` +- 설치파일 받기
+- 윈도우에 설치하기
+- 유닉스/리눅스에 설치하기
+- 설치 후 작업
+- 디렉토리 구조
++
+`,r:{minutes:1.84,words:552},y:"a",t:"4. Tomcat 환경설정"}}],["/05-Software/Tomcat/tomcat101/05-deployment.html",{loader:()=>u(()=>import("./05-deployment.html-B4eAra3Y.js"),__vite__mapDeps([232,1])),meta:{d:164032788e4,g:["Tomcat","Java"],e:` +- 리스너
+- 자바옵션
+- 클래스로더
+- setenv?
+- web.xml
+- 로그
++
+`,r:{minutes:2.35,words:706},y:"a",t:"5. Tomcat 애플리케이션 배포"}}],["/05-Software/Tomcat/tomcat101/06-dbconnection.html",{loader:()=>u(()=>import("./06-dbconnection.html-Lm8jzNRi.js"),__vite__mapDeps([233,1])),meta:{d:164032788e4,g:["Tomcat","Java"],e:` +- Web Application
+- by Manager
+- by webapps DIR
+- by context.xml
+- ROOT
+- Auto Deployment & Hot Deployment
+- Parallel Deployment
++
+`,r:{minutes:1.56,words:468},y:"a",t:"6. Tomcat Database 연동"}}],["/05-Software/Tomcat/tomcat101/07-host.html",{loader:()=>u(()=>import("./07-host.html-C9EMg1b9.js"),__vite__mapDeps([234,1])),meta:{d:164032788e4,g:["Tomcat","Java"],e:` +- JDBC Connection Pool
+- DB 연동 예제
+- DB 연동 설정값
+- JNDI Lookup
++
+ +- 호스트 구성
+- 호스트 특징
+- host manager
+
+7.1 호스트 구성
+톰캣에 정의된 바로는
`,r:{minutes:.68,words:205},y:"a",t:"7. Tomcat HOST"}}],["/05-Software/Tomcat/tomcat101/08-webserver.html",{loader:()=>u(()=>import("./08-webserver.html-CfNCoUcb.js"),__vite__mapDeps([235,1])),meta:{d:164032788e4,g:["Tomcat","Java"],e:` +Host
로 정의되나 일반적인 기능으로 표현한다면 가상 호스트(Virtual Host)와 같은 기능 입니다. 특정 host 명, 즉 http url로 서비스를 분기하는 역할을 합니다.server.xml
기본으로 설정되어있는localhost
인 호스트의 내용은 다음과 같습니다.+
+ +- 웹서버 연동의 이유
+- mod_jk
+- 클러스터
+
+8.1 웹서버 연동의 이유
+톰캣 단일로 서비스하는 경우도 있지만 일반적으로 웹서버와 연동하여 사용하는 경우가 보다 더 많습니다. 그 이유를 다음과 같이 정리합니다.
`,r:{minutes:1.51,words:452},y:"a",t:"8. Tomcat 웹서버 연동"}}],["/05-Software/Tomcat/tomcat101/09-thread.html",{loader:()=>u(()=>import("./09-thread.html-XImYqvQZ.js"),__vite__mapDeps([236,1])),meta:{d:164032788e4,g:["Tomcat","Java"],e:` ++
+ +- Thread?
+- 설정
+- Thread Dump
+
+9.1 Thread?
+Thread는 JVM내에 요청된 작업을 동시에 처리하기 위한 작은 cpu라고 볼 수 있습니다. 톰캣에 서비스 처리를 요청하는 경우 해당 요청은 Queue에 쌓여 FIFO로 Thread에 전달되고 Thread에 여유가 있는 경우 Queue에 들어온 요청은 바로 Thread로 전달되어
`,r:{minutes:.83,words:250},y:"a",t:"9. Tomcat 쓰레드"}}],["/05-Software/Tomcat/tomcat101/10-monitoring.html",{loader:()=>u(()=>import("./10-monitoring.html-B9YmIXXL.js"),__vite__mapDeps([237,1])),meta:{d:164032788e4,g:["Tomcat","Java"],e:` +Queue Length
는 0을 유지하지만 Thread가 모두 사용중이여서 더이상의 요청 처리를 하지 못하는 경우 새로 발생한 요청은 Queue에 쌓이면서 지연이 발생합니다.+
+`,r:{minutes:1.2,words:360},y:"a",t:"10. Tomcat 모니터링"}}],["/05-Software/Tomcat/tomcat101/11-tip.html",{loader:()=>u(()=>import("./11-tip.html-DaXXBiss.js"),__vite__mapDeps([238,1])),meta:{d:164032788e4,g:["Tomcat","Java"],e:` +- 모니터링은 왜 하나?
+- 톰캣 기본 모니터링 툴
+- psi-Probe
+- jkstatus
+- visualVM
+- JMC
+- APM
++
+ +- 디렉토리
+- setenv
+- 실행 유저
+- Connector
+
+11.1 디렉토리
`,r:{minutes:.54,words:162},y:"a",t:"11. Tomcat 팁"}}],["/03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/00-overview.html",{loader:()=>u(()=>import("./00-overview.html-BPGT2Xsa.js"),__vite__mapDeps([239,1])),meta:{d:1695042774e3,g:["ncloud","ncp","terraform","workshop"],e:` + +
+과정 안내
++
`,r:{minutes:.5,words:149},y:"a",t:"Workshop 안내"}}],["/03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/01-terraform-intro.html",{loader:()=>u(()=>import("./01-terraform-intro.html-B1MhIda9.js"),__vite__mapDeps([240,1])),meta:{d:1695042774e3,g:["ncloud","ncp","terraform","workshop"],e:` +- +
+이 과정은 IaC 도구인 Terraform을 사용하여 클라우드 리소스를 생성하는 실습(Hands-on)과정입니다.
+- +
+💻 표시는 실제 실습을 수행하는 단계 입니다.
+- +
+사전 준비 사항
++
+- 인터넷 연결이 가능한 사용자 별 랩탑 또는 데스크탑 환경이 필요합니다.
+- 실습을 위한 샘플 코드활용을 위해 github에 접속 가능해야 합니다.
+- Naver Cloud Platform(NCP)에 회원 가입이 필요합니다.
+- 과정을 실행하기 위해서는 NCP 리소스 사용을 위한 크래딧 또는 결재수단 이 필요합니다. 과정 진행을 위해 강사가 크래딧을 제공할 수 있습니다.
+- 실습을 수행하기 위한 랩탑 환경에 코드 편집기(IDE)로 Visual Studio Code 를 활용합니다. +
++
+- 홈페이지 및 다운로드 : https://code.visualstudio.com/
+- +
+컨텐츠
++
+- Terraform 소개
+- Terraform 기본 +
++
+- 💻 Lab - Setup and Basic Usage
+- Terraform 실행 :
+plan
apply
destroy
++
+- 💻 Lab - Terraform in Action
+- 테라폼 프로비저닝 도구 사용 및 구성 +
++
+- 💻 Lab - Terraform으로 프로비저닝 하기
+- 테라폼 상태파일(State)
+- Terraform Cloud +
++
+- 💻 Lab - Terraform Remote State
+NCP 서버를 어떻게 프로비저닝 하죠?
+새로운 NCP의 인스턴스를 프로비저닝 할 수있는 몇 가지 다른 방법을 살펴 보겠습니다. 시작하기 전에 다음을 포함한 몇 가지 기본 정보를 수집해야합니다 (더 많은 옵션이 있습니다).
++
+- 서버 이름
+- 운영 체제 (Image)
+- VM 크기
+- 지리적 위치 (Region)
+- 보안 그룹
+서버 만들기 Method 1: nCloud Console (GUI)
+ +
+서버 만들기 Method 2: nCloud CLI
`,r:{minutes:.95,words:285},y:"a",t:"01. 테라폼 소개"}}],["/03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/02-terraform-basic.html",{loader:()=>u(()=>import("./02-terraform-basic.html-D7TMDab7.js"),__vite__mapDeps([241,1])),meta:{d:1695042774e3,g:["ncloud","ncp","terraform","workshop"],e:` +Terraform 이란?
+ ++
`,r:{minutes:1.24,words:372},y:"a",t:"02. 테라폼 기본"}}],["/03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/02-z-lab_terraform_basic.html",{loader:()=>u(()=>import("./02-z-lab_terraform_basic.html-updYTors.js"),__vite__mapDeps([242,243,1])),meta:{d:1695042774e3,g:["ncloud","ncp","terraform","workshop"],e:` +- +
+Terraform은 오픈 소스 프로비저닝 도구입니다.
++
+- Terraform Github : https://github.com/hashicorp/terraform
+- +
+Go로 작성된 단일 바이너리로 제공됩니다. Terraform은 크로스 플랫폼이며 Linux, Windows 또는 MacOS에서 실행할 수 있습니다.
+- +
+terraform 설치는 쉽습니다. zip 파일을 다운로드하고 압축을 풀고 실행하기 만하면됩니다.
++
+- 다운로드 : https://www.terraform.io/downloads.html
+
+🏡 Moving in - Explore Your Workspace
+@slidestart blood
+Terraform 명령줄 도구는 MacOS, FreeBSD, OpenBSD, Windows, Solaris 및 Linux에서 사용할 수 있습니다.
+
+Terraform 언어는 사람과 기계가 읽을 수 있도록 설계되었습니다.
+
+대부분의 최신 코드 편집기는 Terraform 구문 강조 표시를 지원합니다.
+@slideend
+테라폼 설치 및 구성
`,r:{minutes:2.29,words:687},y:"a",t:"💻 Lab - Setup and Basic Usage"}}],["/03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/03-terraform-in-Action.html",{loader:()=>u(()=>import("./03-terraform-in-Action.html-DJ9Tld7l.js"),__vite__mapDeps([244,1])),meta:{d:1695042774e3,g:["ncloud","ncp","terraform","workshop"],e:` +리소스 분석
+모든 Terraform으로 구성되는 리소스는 정확히 동일한 방식으로 구성됩니다.
+`,r:{minutes:1.35,words:406},y:"a",t:"03. 테라폼 실행"}}],["/03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/03-z-lab_terraform_action.html",{loader:()=>u(()=>import("./03-z-lab_terraform_action.html-Cew6P_D8.js"),__vite__mapDeps([245,243,1])),meta:{d:1695042774e3,g:["ncloud","ncp","terraform","workshop"],e:` +resource type "name" { + parameter = "foo" + parameter2 = "bar" + list = ["one", "two", "three"] +} +
편집기에서 열기
+ ++
+- VSCode를 실행하고 File(파일) 메뉴에서
+Open Folder...
를 클릭합니다.- 앞서 받은 디렉토리내의
+lab02
을 열어줍니다.
+📈 Terraform Graph
+@slidestart blood
+Terraform Graph는 모든 인프라에 대한 시각적 표현을 제공할 수 있습니다.
`,r:{minutes:2.18,words:653},y:"a",t:"💻 Lab - Terraform in Action"}}],["/03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/04-ncp-provisioning-and-configuration.html",{loader:()=>u(()=>import("./04-ncp-provisioning-and-configuration.html-C77V6Qkn.js"),__vite__mapDeps([246,1])),meta:{d:1695042774e3,g:["ncloud","ncp","terraform","workshop"],e:` +Terraform 프로비저닝 도구 사용
+Terraform을 사용하여 가상 머신 또는 컨테이너를 세우고 나면 운영 체제와 애플리케이션을 구성 할 수 있습니다.
+여기에서 Provisioner 가 등장합니다.
+Terraform은 Bash, Powershell, Chef, Puppet, Ansible 등을 포함한 여러 유형의 Provisioner를 지원합니다.
+https://www.terraform.io/docs/provisioners/index.html
`,r:{minutes:.59,words:177},y:"a",t:"04. 테라폼 프로비저닝 도구 사용 및 구성"}}],["/03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/04-z-lab_provisioners_variables_outputs.html",{loader:()=>u(()=>import("./04-z-lab_provisioners_variables_outputs.html-H3qceguI.js"),__vite__mapDeps([247,243,1])),meta:{d:1695042774e3,g:["ncloud","ncp","terraform","workshop"],e:` +편집기에서 열기
+ ++
+- VSCode를 실행하고 File(파일) 메뉴에서
+Open Folder...
를 클릭합니다.- 앞서 실습을 진행한
+lab02
을 열어줍니다.
+🛠️ Use a Provisioner
+@slidestart blood
+Terraform 프로비저닝 도구는 생성 시 한 번 실행됩니다.
+
`,r:{minutes:.87,words:260},y:"a",t:"💻 Lab - Provisioners, Variables, Outputs"}}],["/03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/05-terraform-state.html",{loader:()=>u(()=>import("./05-terraform-state.html-RoEWncbE.js"),__vite__mapDeps([248,1])),meta:{d:1695042774e3,g:["ncloud","ncp","terraform","workshop"],e:` +Terraform State
+Terraform은 stateful 애플리케이션입니다. 즉, state file 내부에서 빌드 한 모든 내용을 추적합니다.
+앞서의 실습에서 반복된
+Apply
작업 간에 Workspace 디렉토리에 나타난terraform.tfstate
및terraform.tfstate.backup
파일을 보셨을 것입니다.상태 파일은 Terraform이 알고있는 모든 것에 대한 기록 소스입니다.
`,r:{minutes:.48,words:143},y:"a",t:"05. 테라폼 상태파일(State)"}}],["/03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/06-terraform-cloud.html",{loader:()=>u(()=>import("./06-terraform-cloud.html-BOv2Y4DV.js"),__vite__mapDeps([249,1])),meta:{d:1695042774e3,g:["ncloud","ncp","terraform","workshop"],e:` +Terraform Cloud
+Terraform Cloud는 Terraform을 사용하여 코드로 인프라를 작성하고 구축하기위한 최고의 워크 플로를 제공하는 무료 로 시작하는 SaaS 애플리케이션입니다.
+ ++
`,r:{minutes:.38,words:114},y:"a",t:"06. Terraform Cloud"}}],["/03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/06-z-lab_terraform_cloud.html",{loader:()=>u(()=>import("./06-z-lab_terraform_cloud.html-BYSe2KEx.js"),__vite__mapDeps([250,243,1])),meta:{d:1695042774e3,g:["ncloud","ncp","terraform","workshop"],e:` +- State 저장 및 관리
+- Terraform 실행을보고 승인하기위한 웹 UI
+- 개인 모듈 레지스트리
+- VCS (Version Control System) 통합
+- CLI, API 또는 GUI 기반 작업
+- 실행 이벤트 알림
+- 자동화를위한 전체 HTTP API
+편집기에서 열기
+ ++
+- VSCode를 실행하고 File(파일) 메뉴에서
+Open Folder...
를 클릭합니다.- 앞서 실습을 진행한
+lab02
을 열어줍니다.
+☁️ Terraform Configuration
+@slidestart blood
+Terraform Cloud
+Remote State 저장소는 모든 사용자에게 무료입니다.
`,r:{minutes:1.32,words:397},y:"a",t:"💻 Lab - Terraform Cloud 연결"}}],["/04-HashiCorp/04-Consul/06-on_Kubernetes/ServiceMesh101/01-Install.html",{loader:()=>u(()=>import("./01-Install.html-8K2VTmo9.js"),__vite__mapDeps([251,1])),meta:{d:1645936869e3,g:["Consul","ServiceMesh","K8s","Kubernetes"],e:` ++`,r:{minutes:.66,words:198},y:"a",t:"01. Install"}}],["/04-HashiCorp/04-Consul/06-on_Kubernetes/ServiceMesh101/02-SideCar.html",{loader:()=>u(()=>import("./02-SideCar.html-Cll9HLhK.js"),__vite__mapDeps([252,1])),meta:{d:1645936869e3,g:["Consul","ServiceMesh","K8s","Kubernetes"],e:` +팁
+실습을 위한 조건은 다음과 같습니다.
++
+- Kubernetes 1.21 이상의 환경
+- Consul binary http://releases.hashicorp.com/consul/
+- Install helm 3
+- Install Kubectl
+- Consul Namespace 테스트는 Enterprise 라이선스가 필요합니다. : http://consul.io/trial
+++팁
+실습을 위한 조건은 다음과 같습니다.
++
+- Consul 이 구성된 Kubernetes 환경
+- 설치 구성 시
+connectInject
이 활성화 되어있어야 합니다.+ +`,r:{minutes:2.14,words:641},y:"a",t:"02. SideCar"}}],["/04-HashiCorp/04-Consul/06-on_Kubernetes/ServiceMesh101/03-use-crd.html",{loader:()=>u(()=>import("./03-use-crd.html-oAT-nlvR.js"),__vite__mapDeps([253,1])),meta:{d:1645936869e3,g:["Consul","ServiceMesh","K8s","Kubernetes"],e:` ++ + +`,r:{minutes:1.86,words:559},y:"a",t:"03. CRD로 Consul Serive Mesh 관리"}}],["/04-HashiCorp/04-Consul/06-on_Kubernetes/ServiceMesh101/04-traffic-management.html",{loader:()=>u(()=>import("./04-traffic-management.html-D79x_2QV.js"),__vite__mapDeps([254,1])),meta:{d:1645936869e3,g:["Consul","ServiceMesh","K8s","Kubernetes"],e:` +실습을 진행하기 위한 디렉토리를 생성합니다.
+mkdir ./traffic +
Service Mesh는 HTTP 프로토콜 상에서 L7으로 동작하게 됩니다. 따라서 기본 프로토콜을 http로 변경합니다.
+`,r:{minutes:2.45,words:736},y:"a",t:"04. 트래픽 관리"}}],["/04-HashiCorp/04-Consul/06-on_Kubernetes/annotation/ingress-and-serviceroute.html",{loader:()=>u(()=>import("./ingress-and-serviceroute.html-CPeITe-L.js"),__vite__mapDeps([255,1])),meta:{d:1645936869e3,g:["Consul","ServiceMesh","K8s","Kubernetes","ingress"],e:` +cat > ./traffic/service-to-service.yaml <<EOF +apiVersion: consul.hashicorp.com/v1alpha1 +kind: ProxyDefaults +metadata: + name: global +spec: + config: + protocol: http +EOF +
Ingress gateway가 8080을 Listen하도록 구성되어있으면, 아래와 같이 해당 포트의 요청을 받을 대상 서비스를 지정합니다.
+`,r:{minutes:.23,words:68},y:"a",t:"Ingress & ServiceRoute"}}],["/04-HashiCorp/04-Consul/06-on_Kubernetes/annotation/multiport.html",{loader:()=>u(()=>import("./multiport.html-BE7Jum3S.js"),__vite__mapDeps([256,1])),meta:{d:1645936869e3,g:["Consul","ServiceMesh","K8s","Kubernetes","annotation"],e:` +apiVersion: consul.hashicorp.com/v1alpha1 +kind: IngressGateway +metadata: + name: ingress-gateway +spec: + listeners: + - port: 8080 + protocol: http + services: + - name: hashicups + hosts: ["*"] +
++Consul Doc : https://www.consul.io/docs/k8s/connect#kubernetes-pods-with-multiple-ports
+`,r:{minutes:.2,words:60},y:"a",t:"Multiport"}}],["/04-HashiCorp/04-Consul/06-on_Kubernetes/configuration/envoy_timeout.html",{loader:()=>u(()=>import("./envoy_timeout.html-CVf-mWCV.js"),__vite__mapDeps([257,1])),meta:{d:1648260872e3,g:["Consul","ServiceMesh","K8s","Kubernetes","timeout"],e:` +
annotation
에 다음과 같이 서비스 이름과 대상 포트를 리스트로 지정합니다.+`,r:{minutes:1.28,words:385},y:"a",t:"Envoy Timeout"}}],["/04-HashiCorp/04-Consul/06-on_Kubernetes/performance/consul-istio.html",{loader:()=>u(()=>import("./consul-istio.html-CFP_PMhV.js"),__vite__mapDeps([258,1])),meta:{d:166385002e4,g:["Consul","Istio","Kubetenetes","k8s","Performance"],e:` +Consul API : https://www.consul.io/api-docs/config
+
+Proxy Default : https://www.consul.io/docs/connect/config-entries/proxy-defaults
+Envoy Integration : https://www.consul.io/docs/connect/proxies/envoy1. 성능 테스트 수행 결과 요약
+Case 2-1
++
+- Consul Ingress Gateway의 resources.limits 와 resources.requests 의 cpu, memory 를 각각 250m / 500Mi 로 수정
+- Istio Default 1527 Requests/sec 대비 1860 Requests/sec 로 약 22% 빠름 (Case 2-1)
+Case 2-2
++
`,r:{minutes:5.33,words:1598},y:"a",t:"Consul vs Istio - Performance Test"}}],["/04-HashiCorp/04-Consul/06-on_Kubernetes/tracing/jaeger_tracing.html",{loader:()=>u(()=>import("./jaeger_tracing.html-bcpPC1Cp.js"),__vite__mapDeps([259,1])),meta:{a:"유형욱",d:1668245149e3,g:["Consul","Jaeger","Tracing","OpenTelemetry","Istio","IngressGateway","Kubetenetes","K8s"],e:` +- Consul Ingress Gateway resource allocation을 Istio와 동률 구성 시,
+- Istio Default 1527 Requests/sec 대비 3002 Requests/sec로 약 196% 빠름 (Case 2-2)
+0. 사전 요구사항
+1) Consul Install
+Jaeger 연동을 위해 Consul on K8s 환경을 구성합니다. 해당 가이드의 경우에는 여기를 참고하세요.
+(1) 시크릿 생성 - 라이센스
++
+- 라이센스 파일 생성 및 시크릿 생성
+`,r:{minutes:3.36,words:1007},y:"a",t:"Jaeger를 활용한 Consul Service Mesh Tracing"}}],["/04-HashiCorp/06-Vault/01-Information/vault-secret-operator/1-vso-overview.html",{loader:()=>u(()=>import("./1-vso-overview.html-CsgoBSJ3.js"),__vite__mapDeps([260,1])),meta:{d:1684599614e3,g:["vault","operator"],e:` +# license파일 생성 +vi consul.lic + +# 생성한 license파일로 secret 생성 +kubectl create secret generic license --from-file='key=./consul.lic' +
++`,r:{minutes:1.08,words:325},y:"a",t:"Vault Secrets Operator 개요"}}],["/04-HashiCorp/06-Vault/01-Information/vault-secret-operator/2-vso-install.html",{loader:()=>u(()=>import("./2-vso-install.html-DEpsuV_3.js"),__vite__mapDeps([261,1])),meta:{d:1684599614e3,g:["vault","operator"],e:` +참고:
+
+현재 Vault 비밀 오퍼레이터는 공개 베타 버전입니다. *here*에서 GitHub 이슈를 개설하여 피드백을 제공해 주세요.++참고:
+
+현재 Vault 비밀 오퍼레이터는 공개 베타 버전입니다. *here*에서 GitHub 이슈를 개설하여 피드백을 제공해 주세요.사전 요구사항
++
`,r:{minutes:.39,words:118},y:"a",t:"Vault Secrets Operator 설치"}}],["/04-HashiCorp/06-Vault/01-Information/vault-secret-operator/3-vso-samples.html",{loader:()=>u(()=>import("./3-vso-samples.html-BFhpQZdK.js"),__vite__mapDeps([262,1])),meta:{d:1684599614e3,g:["vault","operator"],e:` +- Kubernetes 1.22+
+- Vault OSS/Enterprise 1.11+
+++📌 참고:
+
+현재 Vault 비밀 오퍼레이터는 공개 베타 버전입니다. *here*에서 GitHub 이슈를 개설하여 피드백을 제공해 주세요.본 문서는 HashiCorp 공식 GitHub의 Vault Secret Operator 저장소 에서 제공하는 코드를 활용하여 환경구성 및 샘플 애플리케이션 배포/연동에 대한 상세 분석을 제공한다.
`,r:{minutes:6.82,words:2045},y:"a",t:"Vault Secrets Operator 예제실습"}}],["/404.html",{loader:()=>u(()=>import("./404.html-32aGe-oF.js"),__vite__mapDeps([263,1])),meta:{e:`404 Not Found
+`,r:{minutes:.01,words:3},y:"p",t:""}}],["/99-about/",{loader:()=>u(()=>import("./index.html-BLszlFoz.js"),__vite__mapDeps([264,1])),meta:{r:{minutes:0,words:1},y:"p",t:"99 about"}}],["/01-Infrastructure/Container/",{loader:()=>u(()=>import("./index.html-C2W_HHl5.js"),__vite__mapDeps([265,1])),meta:{r:{minutes:0,words:1},y:"p",t:"Container"}}],["/02-PrivatePlatform/OpenShift/",{loader:()=>u(()=>import("./index.html-BE-2qlXB.js"),__vite__mapDeps([266,1])),meta:{r:{minutes:0,words:1},y:"p",t:"Open Shift"}}],["/02-PrivatePlatform/Vsphere/",{loader:()=>u(()=>import("./index.html-Y991T9IQ.js"),__vite__mapDeps([267,1])),meta:{r:{minutes:0,words:1},y:"p",t:"Vsphere"}}],["/06-etc/class/",{loader:()=>u(()=>import("./index.html-9Yp3ciBJ.js"),__vite__mapDeps([268,1])),meta:{r:{minutes:0,words:1},y:"p",t:"Class"}}],["/06-etc/infomation/",{loader:()=>u(()=>import("./index.html-mpKLZ7_c.js"),__vite__mapDeps([269,1])),meta:{r:{minutes:0,words:1},y:"p",t:"Infomation"}}],["/06-etc/mac/",{loader:()=>u(()=>import("./index.html-0pJIksRV.js"),__vite__mapDeps([270,1])),meta:{r:{minutes:0,words:1},y:"p",t:"Mac"}}],["/06-etc/nodejs/",{loader:()=>u(()=>import("./index.html-CAQ-PxMe.js"),__vite__mapDeps([271,1])),meta:{r:{minutes:0,words:1},y:"p",t:"Nodejs"}}],["/01-Infrastructure/Linux/TroubleShooting/",{loader:()=>u(()=>import("./index.html-D7d5L5hY.js"),__vite__mapDeps([272,1])),meta:{r:{minutes:0,words:1},y:"p",t:"Trouble Shooting"}}],["/01-Infrastructure/Linux/",{loader:()=>u(()=>import("./index.html-Cx5G9-KF.js"),__vite__mapDeps([273,1])),meta:{r:{minutes:0,words:1},y:"p",t:"Linux"}}],["/02-PrivatePlatform/Kubernetes/01-Information/",{loader:()=>u(()=>import("./index.html-CIZaqGwJ.js"),__vite__mapDeps([274,1])),meta:{r:{minutes:0,words:1},y:"p",t:"01 Information"}}],["/02-PrivatePlatform/Kubernetes/",{loader:()=>u(()=>import("./index.html--C6VbOaD.js"),__vite__mapDeps([275,1])),meta:{r:{minutes:0,words:1},y:"p",t:"Kubernetes"}}],["/02-PrivatePlatform/Kubernetes/02-Config/",{loader:()=>u(()=>import("./index.html-DoVw34AL.js"),__vite__mapDeps([276,1])),meta:{r:{minutes:0,words:1},y:"p",t:"02 Config"}}],["/02-PrivatePlatform/Kubernetes/05-Kops/",{loader:()=>u(()=>import("./index.html-BLPfZ0m7.js"),__vite__mapDeps([277,1])),meta:{r:{minutes:0,words:1},y:"p",t:"05 Kops"}}],["/02-PrivatePlatform/Kubernetes/06-EKS/",{loader:()=>u(()=>import("./index.html-C1r8iAhN.js"),__vite__mapDeps([278,1])),meta:{r:{minutes:0,words:1},y:"p",t:"06 EKS"}}],["/04-HashiCorp/01-Packer/01-Information/",{loader:()=>u(()=>import("./index.html-DPXvhdU3.js"),__vite__mapDeps([279,1])),meta:{r:{minutes:0,words:1},y:"p",t:"01 Information"}}],["/04-HashiCorp/01-Packer/",{loader:()=>u(()=>import("./index.html-wLf1xqqZ.js"),__vite__mapDeps([280,1])),meta:{r:{minutes:0,words:1},y:"p",t:"01 Packer"}}],["/04-HashiCorp/01-Packer/05-SamplePkr/",{loader:()=>u(()=>import("./index.html-s7Wkl66o.js"),__vite__mapDeps([281,1])),meta:{r:{minutes:0,words:1},y:"p",t:"05 Sample Pkr"}}],["/04-HashiCorp/02-Vagrant/02-Config/",{loader:()=>u(()=>import("./index.html-CcClr91c.js"),__vite__mapDeps([282,1])),meta:{r:{minutes:0,words:1},y:"p",t:"02 Config"}}],["/04-HashiCorp/02-Vagrant/",{loader:()=>u(()=>import("./index.html-B2Ds2mib.js"),__vite__mapDeps([283,1])),meta:{r:{minutes:0,words:1},y:"p",t:"02 Vagrant"}}],["/04-HashiCorp/02-Vagrant/04-TroubleShooting/",{loader:()=>u(()=>import("./index.html-Bf1_GXwC.js"),__vite__mapDeps([284,1])),meta:{r:{minutes:0,words:1},y:"p",t:"04 Trouble Shooting"}}],["/04-HashiCorp/03-Terraform/01-Information/",{loader:()=>u(()=>import("./index.html-DK0THUGL.js"),__vite__mapDeps([285,1])),meta:{r:{minutes:0,words:1},y:"p",t:"01 Information"}}],["/04-HashiCorp/03-Terraform/",{loader:()=>u(()=>import("./index.html-DUVcfygE.js"),__vite__mapDeps([286,1])),meta:{r:{minutes:0,words:1},y:"p",t:"03 Terraform"}}],["/04-HashiCorp/03-Terraform/02-Config/",{loader:()=>u(()=>import("./index.html-CAT5dI5U.js"),__vite__mapDeps([287,1])),meta:{r:{minutes:0,words:1},y:"p",t:"02 Config"}}],["/04-HashiCorp/03-Terraform/03-Sample/",{loader:()=>u(()=>import("./index.html-CEtuUuQF.js"),__vite__mapDeps([288,1])),meta:{r:{minutes:0,words:1},y:"p",t:"03 Sample"}}],["/04-HashiCorp/03-Terraform/04-TroubleShooting/",{loader:()=>u(()=>import("./index.html-DccqpTT9.js"),__vite__mapDeps([289,1])),meta:{r:{minutes:0,words:1},y:"p",t:"04 Trouble Shooting"}}],["/04-HashiCorp/03-Terraform/05-Airgap/",{loader:()=>u(()=>import("./index.html-nCy78rUq.js"),__vite__mapDeps([290,1])),meta:{r:{minutes:0,words:1},y:"p",t:"05 Airgap"}}],["/04-HashiCorp/04-Consul/01-Information/",{loader:()=>u(()=>import("./index.html-CjtAajix.js"),__vite__mapDeps([291,1])),meta:{r:{minutes:0,words:1},y:"p",t:"01 Information"}}],["/04-HashiCorp/04-Consul/",{loader:()=>u(()=>import("./index.html-CJQDv2R5.js"),__vite__mapDeps([292,1])),meta:{r:{minutes:0,words:1},y:"p",t:"04 Consul"}}],["/04-HashiCorp/04-Consul/02-Configuration/",{loader:()=>u(()=>import("./index.html-BNDIkw2K.js"),__vite__mapDeps([293,1])),meta:{r:{minutes:0,words:1},y:"p",t:"02 Configuration"}}],["/04-HashiCorp/04-Consul/03-UseCase/",{loader:()=>u(()=>import("./index.html-DxiOK83r.js"),__vite__mapDeps([294,1])),meta:{r:{minutes:0,words:1},y:"p",t:"03 Use Case"}}],["/04-HashiCorp/04-Consul/04-TroubleShooting/",{loader:()=>u(()=>import("./index.html-BSRJsirl.js"),__vite__mapDeps([295,1])),meta:{r:{minutes:0,words:1},y:"p",t:"04 Trouble Shooting"}}],["/04-HashiCorp/04-Consul/05-Template_Sample/",{loader:()=>u(()=>import("./index.html-DPMcgTsa.js"),__vite__mapDeps([296,1])),meta:{r:{minutes:0,words:1},y:"p",t:"05 Template Sample"}}],["/04-HashiCorp/05-Boundary/01-Install/",{loader:()=>u(()=>import("./index.html-3L0orhhT.js"),__vite__mapDeps([297,1])),meta:{r:{minutes:0,words:1},y:"p",t:"01 Install"}}],["/04-HashiCorp/05-Boundary/",{loader:()=>u(()=>import("./index.html-H3SdDBxu.js"),__vite__mapDeps([298,1])),meta:{r:{minutes:0,words:1},y:"p",t:"05 Boundary"}}],["/04-HashiCorp/05-Boundary/02-Config/",{loader:()=>u(()=>import("./index.html-k-UYW5pW.js"),__vite__mapDeps([299,1])),meta:{r:{minutes:0,words:1},y:"p",t:"02 Config"}}],["/04-HashiCorp/06-Vault/01-Information/",{loader:()=>u(()=>import("./index.html-xIiPoh75.js"),__vite__mapDeps([300,1])),meta:{r:{minutes:0,words:1},y:"p",t:"01 Information"}}],["/04-HashiCorp/06-Vault/",{loader:()=>u(()=>import("./index.html-BY2TY-GN.js"),__vite__mapDeps([301,1])),meta:{r:{minutes:0,words:1},y:"p",t:"06 Vault"}}],["/04-HashiCorp/06-Vault/02-Secret_Engine/",{loader:()=>u(()=>import("./index.html-mfa_0e2S.js"),__vite__mapDeps([302,1])),meta:{r:{minutes:0,words:1},y:"p",t:"02 Secret Engine"}}],["/04-HashiCorp/06-Vault/03-Auth_Method/",{loader:()=>u(()=>import("./index.html-BILRmv8l.js"),__vite__mapDeps([303,1])),meta:{r:{minutes:0,words:1},y:"p",t:"03 Auth Method"}}],["/04-HashiCorp/06-Vault/04-UseCase/",{loader:()=>u(()=>import("./index.html-CqOAKbz8.js"),__vite__mapDeps([304,1])),meta:{r:{minutes:0,words:1},y:"p",t:"04 Use Case"}}],["/04-HashiCorp/06-Vault/05-TroubleShooting/",{loader:()=>u(()=>import("./index.html-b1v4IoTd.js"),__vite__mapDeps([305,1])),meta:{r:{minutes:0,words:1},y:"p",t:"05 Trouble Shooting"}}],["/04-HashiCorp/06-Vault/06-Config/",{loader:()=>u(()=>import("./index.html-CgmgeYBj.js"),__vite__mapDeps([306,1])),meta:{r:{minutes:0,words:1},y:"p",t:"06 Config"}}],["/04-HashiCorp/06-Vault/07-Sentinel-Sample/",{loader:()=>u(()=>import("./index.html-BNrxTMC6.js"),__vite__mapDeps([307,1])),meta:{r:{minutes:0,words:1},y:"p",t:"07 Sentinel Sample"}}],["/04-HashiCorp/07-Nomad/01-Information/",{loader:()=>u(()=>import("./index.html-BBKmscZU.js"),__vite__mapDeps([308,1])),meta:{r:{minutes:0,words:1},y:"p",t:"01 Information"}}],["/04-HashiCorp/07-Nomad/",{loader:()=>u(()=>import("./index.html-CQLB9tTY.js"),__vite__mapDeps([309,1])),meta:{r:{minutes:0,words:1},y:"p",t:"07 Nomad"}}],["/04-HashiCorp/07-Nomad/02-Config/",{loader:()=>u(()=>import("./index.html-D_LPTw71.js"),__vite__mapDeps([310,1])),meta:{r:{minutes:0,words:1},y:"p",t:"02 Config"}}],["/04-HashiCorp/07-Nomad/04-UseCase/",{loader:()=>u(()=>import("./index.html-DS6XhucT.js"),__vite__mapDeps([311,1])),meta:{r:{minutes:0,words:1},y:"p",t:"04 Use Case"}}],["/04-HashiCorp/07-Nomad/05-SampleJob/",{loader:()=>u(()=>import("./index.html-DSS4UbEp.js"),__vite__mapDeps([312,1])),meta:{r:{minutes:0,words:1},y:"p",t:"05 Sample Job"}}],["/04-HashiCorp/08-Updates/97-2024/",{loader:()=>u(()=>import("./index.html-D_OTBbrV.js"),__vite__mapDeps([313,1])),meta:{r:{minutes:0,words:1},y:"p",t:"97 2024"}}],["/04-HashiCorp/08-Updates/",{loader:()=>u(()=>import("./index.html-8qOH7jzh.js"),__vite__mapDeps([314,1])),meta:{r:{minutes:0,words:1},y:"p",t:"08 Updates"}}],["/04-HashiCorp/08-Updates/98-2023/",{loader:()=>u(()=>import("./index.html-BmBdcU_O.js"),__vite__mapDeps([315,1])),meta:{r:{minutes:0,words:1},y:"p",t:"98 2023"}}],["/04-HashiCorp/08-Updates/99-2022/",{loader:()=>u(()=>import("./index.html-By9mFuaz.js"),__vite__mapDeps([316,1])),meta:{r:{minutes:0,words:1},y:"p",t:"99 2022"}}],["/05-Software/Jenkins/pipeline101/",{loader:()=>u(()=>import("./index.html-CLwoL5XG.js"),__vite__mapDeps([317,1])),meta:{r:{minutes:0,words:1},y:"p",t:"Pipeline101"}}],["/05-Software/Jenkins/",{loader:()=>u(()=>import("./index.html-Ch-G6YLD.js"),__vite__mapDeps([318,1])),meta:{r:{minutes:0,words:1},y:"p",t:"Jenkins"}}],["/05-Software/Tomcat/tomcat101/",{loader:()=>u(()=>import("./index.html-7hyYvoEG.js"),__vite__mapDeps([319,1])),meta:{r:{minutes:0,words:1},y:"p",t:"Tomcat101"}}],["/05-Software/Tomcat/",{loader:()=>u(()=>import("./index.html-GlXEfwY0.js"),__vite__mapDeps([320,1])),meta:{r:{minutes:0,words:1},y:"p",t:"Tomcat"}}],["/03-PublicCloud/NCP/09-Terraform-Workshop/01-intro_to_terraform_on_ncp/",{loader:()=>u(()=>import("./index.html-KDl0Hsol.js"),__vite__mapDeps([321,1])),meta:{r:{minutes:0,words:1},y:"p",t:"01 Intro to Terraform on Ncp"}}],["/03-PublicCloud/NCP/09-Terraform-Workshop/",{loader:()=>u(()=>import("./index.html-DaXtS7sB.js"),__vite__mapDeps([322,1])),meta:{r:{minutes:0,words:1},y:"p",t:"09 Terraform Workshop"}}],["/04-HashiCorp/04-Consul/06-on_Kubernetes/ServiceMesh101/",{loader:()=>u(()=>import("./index.html-a1dbSbEr.js"),__vite__mapDeps([323,1])),meta:{r:{minutes:0,words:1},y:"p",t:"Service Mesh101"}}],["/04-HashiCorp/04-Consul/06-on_Kubernetes/",{loader:()=>u(()=>import("./index.html-DxPYE0xl.js"),__vite__mapDeps([324,1])),meta:{r:{minutes:0,words:1},y:"p",t:"06 on Kubernetes"}}],["/04-HashiCorp/04-Consul/06-on_Kubernetes/annotation/",{loader:()=>u(()=>import("./index.html-DagDfy_4.js"),__vite__mapDeps([325,1])),meta:{r:{minutes:0,words:1},y:"p",t:"Annotation"}}],["/04-HashiCorp/04-Consul/06-on_Kubernetes/configuration/",{loader:()=>u(()=>import("./index.html-Bz3HFByO.js"),__vite__mapDeps([326,1])),meta:{r:{minutes:0,words:1},y:"p",t:"Configuration"}}],["/04-HashiCorp/04-Consul/06-on_Kubernetes/performance/",{loader:()=>u(()=>import("./index.html-bUpryGxr.js"),__vite__mapDeps([327,1])),meta:{r:{minutes:0,words:1},y:"p",t:"Performance"}}],["/04-HashiCorp/04-Consul/06-on_Kubernetes/tracing/",{loader:()=>u(()=>import("./index.html-bct9jB7A.js"),__vite__mapDeps([328,1])),meta:{r:{minutes:0,words:1},y:"p",t:"Tracing"}}],["/04-HashiCorp/06-Vault/01-Information/vault-secret-operator/",{loader:()=>u(()=>import("./index.html-DA0fCO1A.js"),__vite__mapDeps([329,1])),meta:{r:{minutes:0,words:1},y:"p",t:"Vault Secret Operator"}}],["/category/",{loader:()=>u(()=>import("./index.html-VPpMoLau.js"),__vite__mapDeps([330,1])),meta:{y:"p",t:"카테고리",I:!1}}],["/tag/",{loader:()=>u(()=>import("./index.html-zOoKtAv4.js"),__vite__mapDeps([331,1])),meta:{y:"p",t:"태그",I:!1}}],["/tag/infrastructure/",{loader:()=>u(()=>import("./index.html-B4dOH6ES.js"),__vite__mapDeps([332,1])),meta:{y:"p",t:"태그: Infrastructure",I:!1}}],["/tag/platform/",{loader:()=>u(()=>import("./index.html-oftUuUOw.js"),__vite__mapDeps([333,1])),meta:{y:"p",t:"태그: Platform",I:!1}}],["/tag/cloud/",{loader:()=>u(()=>import("./index.html-CTrkKBqc.js"),__vite__mapDeps([334,1])),meta:{y:"p",t:"태그: Cloud",I:!1}}],["/tag/hashicorp/",{loader:()=>u(()=>import("./index.html-Cfko_1rJ.js"),__vite__mapDeps([335,1])),meta:{y:"p",t:"태그: HashiCorp",I:!1}}],["/tag/software/",{loader:()=>u(()=>import("./index.html-BF_aYaEp.js"),__vite__mapDeps([336,1])),meta:{y:"p",t:"태그: Software",I:!1}}],["/tag/etc/",{loader:()=>u(()=>import("./index.html-CpSUNMQp.js"),__vite__mapDeps([337,1])),meta:{y:"p",t:"태그: Etc",I:!1}}],["/tag/container/",{loader:()=>u(()=>import("./index.html-CTJxT9up.js"),__vite__mapDeps([338,1])),meta:{y:"p",t:"태그: container",I:!1}}],["/tag/docker/",{loader:()=>u(()=>import("./index.html-pQy4XdCb.js"),__vite__mapDeps([339,1])),meta:{y:"p",t:"태그: docker",I:!1}}],["/tag/podman/",{loader:()=>u(()=>import("./index.html-2cA91sb6.js"),__vite__mapDeps([340,1])),meta:{y:"p",t:"태그: podman",I:!1}}],["/tag/rancher/",{loader:()=>u(()=>import("./index.html-Y5zHI8Sj.js"),__vite__mapDeps([341,1])),meta:{y:"p",t:"태그: rancher",I:!1}}],["/tag/mac/",{loader:()=>u(()=>import("./index.html-CyblnQzA.js"),__vite__mapDeps([342,1])),meta:{y:"p",t:"태그: mac",I:!1}}],["/tag/openshift/",{loader:()=>u(()=>import("./index.html-CEah2Pie.js"),__vite__mapDeps([343,1])),meta:{y:"p",t:"태그: openshift",I:!1}}],["/tag/ocp/",{loader:()=>u(()=>import("./index.html-C5_BWQA9.js"),__vite__mapDeps([344,1])),meta:{y:"p",t:"태그: ocp",I:!1}}],["/tag/jboss/",{loader:()=>u(()=>import("./index.html-CmlIA75P.js"),__vite__mapDeps([345,1])),meta:{y:"p",t:"태그: jboss",I:!1}}],["/tag/vsphere/",{loader:()=>u(()=>import("./index.html-BQjr9HCF.js"),__vite__mapDeps([346,1])),meta:{y:"p",t:"태그: vsphere",I:!1}}],["/tag/template/",{loader:()=>u(()=>import("./index.html-Cv-VJgI7.js"),__vite__mapDeps([347,1])),meta:{y:"p",t:"태그: template",I:!1}}],["/tag/alibaba/",{loader:()=>u(()=>import("./index.html-BBIP4ZL9.js"),__vite__mapDeps([348,1])),meta:{y:"p",t:"태그: alibaba",I:!1}}],["/tag/aliyun/",{loader:()=>u(()=>import("./index.html-BEV56aKZ.js"),__vite__mapDeps([349,1])),meta:{y:"p",t:"태그: aliyun",I:!1}}],["/tag/devops/",{loader:()=>u(()=>import("./index.html-DwFTYg5y.js"),__vite__mapDeps([350,1])),meta:{y:"p",t:"태그: devops",I:!1}}],["/tag/ai/",{loader:()=>u(()=>import("./index.html-C6KvNayw.js"),__vite__mapDeps([351,1])),meta:{y:"p",t:"태그: ai",I:!1}}],["/tag/keyboard/",{loader:()=>u(()=>import("./index.html-BQTG7BuQ.js"),__vite__mapDeps([352,1])),meta:{y:"p",t:"태그: keyboard",I:!1}}],["/tag/tip/",{loader:()=>u(()=>import("./index.html-BJZy3j7v.js"),__vite__mapDeps([353,1])),meta:{y:"p",t:"태그: tip",I:!1}}],["/tag/acronyms/",{loader:()=>u(()=>import("./index.html-B-pgB9pG.js"),__vite__mapDeps([354,1])),meta:{y:"p",t:"태그: acronyms",I:!1}}],["/tag/homebrew/",{loader:()=>u(()=>import("./index.html-fzDvWzlC.js"),__vite__mapDeps([355,1])),meta:{y:"p",t:"태그: homebrew",I:!1}}],["/tag/brew/",{loader:()=>u(()=>import("./index.html-DuuvZ83O.js"),__vite__mapDeps([356,1])),meta:{y:"p",t:"태그: brew",I:!1}}],["/tag/wget/",{loader:()=>u(()=>import("./index.html-C_kVuTQi.js"),__vite__mapDeps([357,1])),meta:{y:"p",t:"태그: wget",I:!1}}],["/tag/arm/",{loader:()=>u(()=>import("./index.html-BggXIKDt.js"),__vite__mapDeps([358,1])),meta:{y:"p",t:"태그: arm",I:!1}}],["/tag/nodejs/",{loader:()=>u(()=>import("./index.html-DLMLVJ8R.js"),__vite__mapDeps([359,1])),meta:{y:"p",t:"태그: nodejs",I:!1}}],["/tag/linux/",{loader:()=>u(()=>import("./index.html-D5Lk4DVr.js"),__vite__mapDeps([360,1])),meta:{y:"p",t:"태그: linux",I:!1}}],["/tag/oom/",{loader:()=>u(()=>import("./index.html-BvOBsnWu.js"),__vite__mapDeps([361,1])),meta:{y:"p",t:"태그: oom",I:!1}}],["/tag/oom-killer/",{loader:()=>u(()=>import("./index.html-D4qKC5cg.js"),__vite__mapDeps([362,1])),meta:{y:"p",t:"태그: oom_killer",I:!1}}],["/tag/ssh/",{loader:()=>u(()=>import("./index.html-unUkXPF2.js"),__vite__mapDeps([363,1])),meta:{y:"p",t:"태그: ssh",I:!1}}],["/tag/bridge/",{loader:()=>u(()=>import("./index.html-Dx2kFXDd.js"),__vite__mapDeps([364,1])),meta:{y:"p",t:"태그: bridge",I:!1}}],["/tag/netstat/",{loader:()=>u(()=>import("./index.html-BqKRlohc.js"),__vite__mapDeps([365,1])),meta:{y:"p",t:"태그: netstat",I:!1}}],["/tag/kubernetes/",{loader:()=>u(()=>import("./index.html-D_BQsV-j.js"),__vite__mapDeps([366,1])),meta:{y:"p",t:"태그: kubernetes",I:!1}}],["/tag/scheduler/",{loader:()=>u(()=>import("./index.html-B2uIxBsv.js"),__vite__mapDeps([367,1])),meta:{y:"p",t:"태그: scheduler",I:!1}}],["/tag/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98/",{loader:()=>u(()=>import("./index.html-BsAHPhKo.js"),__vite__mapDeps([368,1])),meta:{y:"p",t:"태그: 알고리즘",I:!1}}],["/tag/docker%EC%95%84%EB%8B%98/",{loader:()=>u(()=>import("./index.html-DyLF8uD9.js"),__vite__mapDeps([369,1])),meta:{y:"p",t:"태그: docker아님",I:!1}}],["/tag/containerd/",{loader:()=>u(()=>import("./index.html-C4YGXnQS.js"),__vite__mapDeps([370,1])),meta:{y:"p",t:"태그: containerd",I:!1}}],["/tag/vagrant/",{loader:()=>u(()=>import("./index.html-CxTJPFTW.js"),__vite__mapDeps([371,1])),meta:{y:"p",t:"태그: vagrant",I:!1}}],["/tag/install/",{loader:()=>u(()=>import("./index.html-B6FRUmhJ.js"),__vite__mapDeps([372,1])),meta:{y:"p",t:"태그: install",I:!1}}],["/tag/kubernetes/",{loader:()=>u(()=>import("./index.html-D_BQsV-j.js"),__vite__mapDeps([366,1])),meta:{y:"p",t:"태그: Kubernetes",I:!1}}],["/tag/kops/",{loader:()=>u(()=>import("./index.html-i23O-Qtk.js"),__vite__mapDeps([373,1])),meta:{y:"p",t:"태그: Kops",I:!1}}],["/tag/eks/",{loader:()=>u(()=>import("./index.html-BX35AjeC.js"),__vite__mapDeps([374,1])),meta:{y:"p",t:"태그: EKS",I:!1}}],["/tag/pkos/",{loader:()=>u(()=>import("./index.html-CmLSUAEl.js"),__vite__mapDeps([375,1])),meta:{y:"p",t:"태그: PKOS",I:!1}}],["/tag/packer/",{loader:()=>u(()=>import("./index.html-5fghFVL4.js"),__vite__mapDeps([376,1])),meta:{y:"p",t:"태그: Packer",I:!1}}],["/tag/hcp/",{loader:()=>u(()=>import("./index.html-euWvXoZN.js"),__vite__mapDeps([377,1])),meta:{y:"p",t:"태그: HCP",I:!1}}],["/tag/terraform/",{loader:()=>u(()=>import("./index.html-CQIz16sa.js"),__vite__mapDeps([378,1])),meta:{y:"p",t:"태그: Terraform",I:!1}}],["/tag/sample/",{loader:()=>u(()=>import("./index.html-CdmsQh5K.js"),__vite__mapDeps([379,1])),meta:{y:"p",t:"태그: Sample",I:!1}}],["/tag/alibaba/",{loader:()=>u(()=>import("./index.html-BBIP4ZL9.js"),__vite__mapDeps([348,1])),meta:{y:"p",t:"태그: Alibaba",I:!1}}],["/tag/azure/",{loader:()=>u(()=>import("./index.html-CJqbrHat.js"),__vite__mapDeps([380,1])),meta:{y:"p",t:"태그: Azure",I:!1}}],["/tag/gcp/",{loader:()=>u(()=>import("./index.html-DIqfsULZ.js"),__vite__mapDeps([381,1])),meta:{y:"p",t:"태그: GCP",I:!1}}],["/tag/aws/",{loader:()=>u(()=>import("./index.html-CvidcWV7.js"),__vite__mapDeps([382,1])),meta:{y:"p",t:"태그: aws",I:!1}}],["/tag/ncp/",{loader:()=>u(()=>import("./index.html-BF9sdaEF.js"),__vite__mapDeps([383,1])),meta:{y:"p",t:"태그: NCP",I:!1}}],["/tag/virtualbox/",{loader:()=>u(()=>import("./index.html-MJbxI0SS.js"),__vite__mapDeps([384,1])),meta:{y:"p",t:"태그: virtualbox",I:!1}}],["/tag/terraform/",{loader:()=>u(()=>import("./index.html-CQIz16sa.js"),__vite__mapDeps([378,1])),meta:{y:"p",t:"태그: terraform",I:!1}}],["/tag/iac/",{loader:()=>u(()=>import("./index.html-CZz7gIva.js"),__vite__mapDeps([385,1])),meta:{y:"p",t:"태그: IaC",I:!1}}],["/tag/usecase/",{loader:()=>u(()=>import("./index.html-NHgQ8Gu7.js"),__vite__mapDeps([386,1])),meta:{y:"p",t:"태그: usecase",I:!1}}],["/tag/hcl/",{loader:()=>u(()=>import("./index.html-BsqDco2-.js"),__vite__mapDeps([387,1])),meta:{y:"p",t:"태그: HCL",I:!1}}],["/tag/admin/",{loader:()=>u(()=>import("./index.html-keoyN10d.js"),__vite__mapDeps([388,1])),meta:{y:"p",t:"태그: admin",I:!1}}],["/tag/password/",{loader:()=>u(()=>import("./index.html-C0wy1dkA.js"),__vite__mapDeps([389,1])),meta:{y:"p",t:"태그: password",I:!1}}],["/tag/terraform-on-azure/",{loader:()=>u(()=>import("./index.html-BG1Pjsto.js"),__vite__mapDeps([390,1])),meta:{y:"p",t:"태그: Terraform on Azure",I:!1}}],["/tag/hashicat/",{loader:()=>u(()=>import("./index.html-6sDacYwW.js"),__vite__mapDeps([391,1])),meta:{y:"p",t:"태그: HashiCat",I:!1}}],["/tag/terraform-oss/",{loader:()=>u(()=>import("./index.html-CtzrHCK3.js"),__vite__mapDeps([392,1])),meta:{y:"p",t:"태그: Terraform OSS",I:!1}}],["/tag/terraform-cloud/",{loader:()=>u(()=>import("./index.html-CFrMvdws.js"),__vite__mapDeps([393,1])),meta:{y:"p",t:"태그: Terraform Cloud",I:!1}}],["/tag/terraform-enterprise/",{loader:()=>u(()=>import("./index.html-BF52RzPf.js"),__vite__mapDeps([394,1])),meta:{y:"p",t:"태그: Terraform Enterprise",I:!1}}],["/tag/terraform-%EC%83%98%ED%94%8C/",{loader:()=>u(()=>import("./index.html-DJ4D0DJh.js"),__vite__mapDeps([395,1])),meta:{y:"p",t:"태그: Terraform 샘플",I:!1}}],["/tag/nomad/",{loader:()=>u(()=>import("./index.html-B8KgkMBY.js"),__vite__mapDeps([396,1])),meta:{y:"p",t:"태그: Nomad",I:!1}}],["/tag/terrafom/",{loader:()=>u(()=>import("./index.html-B1pnXePm.js"),__vite__mapDeps([397,1])),meta:{y:"p",t:"태그: terrafom",I:!1}}],["/tag/csi/",{loader:()=>u(()=>import("./index.html-S8M0EGI4.js"),__vite__mapDeps([398,1])),meta:{y:"p",t:"태그: CSI",I:!1}}],["/tag/state/",{loader:()=>u(()=>import("./index.html-Bq8qrdzR.js"),__vite__mapDeps([399,1])),meta:{y:"p",t:"태그: State",I:!1}}],["/tag/enterprise/",{loader:()=>u(()=>import("./index.html-S9AQkxl9.js"),__vite__mapDeps([400,1])),meta:{y:"p",t:"태그: Enterprise",I:!1}}],["/tag/tfe/",{loader:()=>u(()=>import("./index.html-B9XFa-ze.js"),__vite__mapDeps([401,1])),meta:{y:"p",t:"태그: TFE",I:!1}}],["/tag/provider/",{loader:()=>u(()=>import("./index.html-CAuA_CTO.js"),__vite__mapDeps([402,1])),meta:{y:"p",t:"태그: provider",I:!1}}],["/tag/consul/",{loader:()=>u(()=>import("./index.html-D99KEvGE.js"),__vite__mapDeps([403,1])),meta:{y:"p",t:"태그: Consul",I:!1}}],["/tag/consul/",{loader:()=>u(()=>import("./index.html-D99KEvGE.js"),__vite__mapDeps([403,1])),meta:{y:"p",t:"태그: consul",I:!1}}],["/tag/sizing/",{loader:()=>u(()=>import("./index.html-BopX_040.js"),__vite__mapDeps([404,1])),meta:{y:"p",t:"태그: sizing",I:!1}}],["/tag/port/",{loader:()=>u(()=>import("./index.html-BuaeLR9j.js"),__vite__mapDeps([405,1])),meta:{y:"p",t:"태그: port",I:!1}}],["/tag/requirement/",{loader:()=>u(()=>import("./index.html-BR364U9h.js"),__vite__mapDeps([406,1])),meta:{y:"p",t:"태그: requirement",I:!1}}],["/tag/configuration/",{loader:()=>u(()=>import("./index.html-C5n-WiG3.js"),__vite__mapDeps([407,1])),meta:{y:"p",t:"태그: Configuration",I:!1}}],["/tag/forwarddns/",{loader:()=>u(()=>import("./index.html-bvyp_l4x.js"),__vite__mapDeps([408,1])),meta:{y:"p",t:"태그: ForwardDns",I:!1}}],["/tag/acl/",{loader:()=>u(()=>import("./index.html-CeLFvJF4.js"),__vite__mapDeps([409,1])),meta:{y:"p",t:"태그: Acl",I:!1}}],["/tag/policy/",{loader:()=>u(()=>import("./index.html-ByKWy6GY.js"),__vite__mapDeps([410,1])),meta:{y:"p",t:"태그: Policy",I:!1}}],["/tag/client/",{loader:()=>u(()=>import("./index.html-6uHBwJuN.js"),__vite__mapDeps([411,1])),meta:{y:"p",t:"태그: Client",I:!1}}],["/tag/common/",{loader:()=>u(()=>import("./index.html-Cy0Tg075.js"),__vite__mapDeps([412,1])),meta:{y:"p",t:"태그: Common",I:!1}}],["/tag/server/",{loader:()=>u(()=>import("./index.html-B9Iyza3Y.js"),__vite__mapDeps([413,1])),meta:{y:"p",t:"태그: Server",I:!1}}],["/tag/hybrid/",{loader:()=>u(()=>import("./index.html-BduOr1T1.js"),__vite__mapDeps([414,1])),meta:{y:"p",t:"태그: Hybrid",I:!1}}],["/tag/kubetenetes/",{loader:()=>u(()=>import("./index.html-CqIGFBPh.js"),__vite__mapDeps([415,1])),meta:{y:"p",t:"태그: Kubetenetes",I:!1}}],["/tag/k8s/",{loader:()=>u(()=>import("./index.html-CikwTvHf.js"),__vite__mapDeps([416,1])),meta:{y:"p",t:"태그: k8s",I:!1}}],["/tag/vm/",{loader:()=>u(()=>import("./index.html-C_qE9ILI.js"),__vite__mapDeps([417,1])),meta:{y:"p",t:"태그: VM",I:!1}}],["/tag/servicemesh/",{loader:()=>u(()=>import("./index.html--QJEMUXu.js"),__vite__mapDeps([418,1])),meta:{y:"p",t:"태그: ServiceMesh",I:!1}}],["/tag/sidecar/",{loader:()=>u(()=>import("./index.html-B4LLQU-B.js"),__vite__mapDeps([419,1])),meta:{y:"p",t:"태그: SideCar",I:!1}}],["/tag/k8s/",{loader:()=>u(()=>import("./index.html-CikwTvHf.js"),__vite__mapDeps([416,1])),meta:{y:"p",t:"태그: K8S",I:!1}}],["/tag/consul-template/",{loader:()=>u(()=>import("./index.html-BjFvZ5a7.js"),__vite__mapDeps([420,1])),meta:{y:"p",t:"태그: Consul Template",I:!1}}],["/tag/nginx/",{loader:()=>u(()=>import("./index.html-DcotvADo.js"),__vite__mapDeps([421,1])),meta:{y:"p",t:"태그: NGINX",I:!1}}],["/tag/boundary/",{loader:()=>u(()=>import("./index.html-C8INjfIw.js"),__vite__mapDeps([422,1])),meta:{y:"p",t:"태그: Boundary",I:!1}}],["/tag/install/",{loader:()=>u(()=>import("./index.html-B6FRUmhJ.js"),__vite__mapDeps([372,1])),meta:{y:"p",t:"태그: Install",I:!1}}],["/tag/config/",{loader:()=>u(()=>import("./index.html-ChG7pV1C.js"),__vite__mapDeps([423,1])),meta:{y:"p",t:"태그: Config",I:!1}}],["/tag/vault/",{loader:()=>u(()=>import("./index.html-SJgMEq6R.js"),__vite__mapDeps([424,1])),meta:{y:"p",t:"태그: vault",I:!1}}],["/tag/kmip/",{loader:()=>u(()=>import("./index.html-DimCrq4r.js"),__vite__mapDeps([425,1])),meta:{y:"p",t:"태그: kmip",I:!1}}],["/tag/audit/",{loader:()=>u(()=>import("./index.html-BiGgiuzi.js"),__vite__mapDeps([426,1])),meta:{y:"p",t:"태그: audit",I:!1}}],["/tag/optinos/",{loader:()=>u(()=>import("./index.html-Dcf1IyfU.js"),__vite__mapDeps([427,1])),meta:{y:"p",t:"태그: optinos",I:!1}}],["/tag/configuration/",{loader:()=>u(()=>import("./index.html-C5n-WiG3.js"),__vite__mapDeps([407,1])),meta:{y:"p",t:"태그: configuration",I:!1}}],["/tag/token/",{loader:()=>u(()=>import("./index.html-DM6D9wmd.js"),__vite__mapDeps([428,1])),meta:{y:"p",t:"태그: token",I:!1}}],["/tag/vault-enterprise/",{loader:()=>u(()=>import("./index.html-B2XQD821.js"),__vite__mapDeps([429,1])),meta:{y:"p",t:"태그: Vault Enterprise",I:!1}}],["/tag/keymgmt/",{loader:()=>u(()=>import("./index.html-DwOJNE_j.js"),__vite__mapDeps([430,1])),meta:{y:"p",t:"태그: keymgmt",I:!1}}],["/tag/kmip/",{loader:()=>u(()=>import("./index.html-DimCrq4r.js"),__vite__mapDeps([425,1])),meta:{y:"p",t:"태그: KMIP",I:!1}}],["/tag/mongodb/",{loader:()=>u(()=>import("./index.html-Jw7ubVlF.js"),__vite__mapDeps([431,1])),meta:{y:"p",t:"태그: MongoDB",I:!1}}],["/tag/pki/",{loader:()=>u(()=>import("./index.html-vsoKrW4I.js"),__vite__mapDeps([432,1])),meta:{y:"p",t:"태그: PKI",I:!1}}],["/tag/ssh/",{loader:()=>u(()=>import("./index.html-unUkXPF2.js"),__vite__mapDeps([363,1])),meta:{y:"p",t:"태그: SSH",I:!1}}],["/tag/otp/",{loader:()=>u(()=>import("./index.html-CyxdOIsk.js"),__vite__mapDeps([433,1])),meta:{y:"p",t:"태그: OTP",I:!1}}],["/tag/debian/",{loader:()=>u(()=>import("./index.html-jiFMwsKl.js"),__vite__mapDeps([434,1])),meta:{y:"p",t:"태그: Debian",I:!1}}],["/tag/ubuntu/",{loader:()=>u(()=>import("./index.html-vUYXlQFs.js"),__vite__mapDeps([435,1])),meta:{y:"p",t:"태그: Ubuntu",I:!1}}],["/tag/rocky/",{loader:()=>u(()=>import("./index.html-Ja94iRA2.js"),__vite__mapDeps([436,1])),meta:{y:"p",t:"태그: Rocky",I:!1}}],["/tag/rhel/",{loader:()=>u(()=>import("./index.html-CUrRr4Pv.js"),__vite__mapDeps([437,1])),meta:{y:"p",t:"태그: RHEL",I:!1}}],["/tag/centos/",{loader:()=>u(()=>import("./index.html-BxluqjPf.js"),__vite__mapDeps([438,1])),meta:{y:"p",t:"태그: CentOS",I:!1}}],["/tag/transform/",{loader:()=>u(()=>import("./index.html-CfQGYS3C.js"),__vite__mapDeps([439,1])),meta:{y:"p",t:"태그: transform",I:!1}}],["/tag/fpe/",{loader:()=>u(()=>import("./index.html-DgTVepx_.js"),__vite__mapDeps([440,1])),meta:{y:"p",t:"태그: fpe",I:!1}}],["/tag/transit/",{loader:()=>u(()=>import("./index.html-C7Glq06n.js"),__vite__mapDeps([441,1])),meta:{y:"p",t:"태그: transit",I:!1}}],["/tag/vault-auth/",{loader:()=>u(()=>import("./index.html-DJjESVmS.js"),__vite__mapDeps([442,1])),meta:{y:"p",t:"태그: vault auth",I:!1}}],["/tag/aws/",{loader:()=>u(()=>import("./index.html-CvidcWV7.js"),__vite__mapDeps([382,1])),meta:{y:"p",t:"태그: AWS",I:!1}}],["/tag/mfa/",{loader:()=>u(()=>import("./index.html-WpJ-gk6q.js"),__vite__mapDeps([443,1])),meta:{y:"p",t:"태그: MFA",I:!1}}],["/tag/kv/",{loader:()=>u(()=>import("./index.html-MVPqhWoV.js"),__vite__mapDeps([444,1])),meta:{y:"p",t:"태그: kv",I:!1}}],["/tag/policy/",{loader:()=>u(()=>import("./index.html-ByKWy6GY.js"),__vite__mapDeps([410,1])),meta:{y:"p",t:"태그: policy",I:!1}}],["/tag/argocd/",{loader:()=>u(()=>import("./index.html-CY6KWj9G.js"),__vite__mapDeps([445,1])),meta:{y:"p",t:"태그: argocd",I:!1}}],["/tag/gitops/",{loader:()=>u(()=>import("./index.html-CvWV6fvZ.js"),__vite__mapDeps([446,1])),meta:{y:"p",t:"태그: gitops",I:!1}}],["/tag/devsescops/",{loader:()=>u(()=>import("./index.html-D1L525B6.js"),__vite__mapDeps([447,1])),meta:{y:"p",t:"태그: devsescops",I:!1}}],["/tag/pipeline/",{loader:()=>u(()=>import("./index.html-BDnVrQ9b.js"),__vite__mapDeps([448,1])),meta:{y:"p",t:"태그: pipeline",I:!1}}],["/tag/github/",{loader:()=>u(()=>import("./index.html-Dr_GfYP4.js"),__vite__mapDeps([449,1])),meta:{y:"p",t:"태그: github",I:!1}}],["/tag/gitlab/",{loader:()=>u(()=>import("./index.html-B6BrFWUb.js"),__vite__mapDeps([450,1])),meta:{y:"p",t:"태그: gitlab",I:!1}}],["/tag/secret/",{loader:()=>u(()=>import("./index.html-CkTazoSW.js"),__vite__mapDeps([451,1])),meta:{y:"p",t:"태그: secret",I:!1}}],["/tag/eks/",{loader:()=>u(()=>import("./index.html-BX35AjeC.js"),__vite__mapDeps([374,1])),meta:{y:"p",t:"태그: eks",I:!1}}],["/tag/jenkins/",{loader:()=>u(()=>import("./index.html-V4RqHKzL.js"),__vite__mapDeps([452,1])),meta:{y:"p",t:"태그: jenkins",I:!1}}],["/tag/approle/",{loader:()=>u(()=>import("./index.html-BnnT--QB.js"),__vite__mapDeps([453,1])),meta:{y:"p",t:"태그: approle",I:!1}}],["/tag/otp/",{loader:()=>u(()=>import("./index.html-CyxdOIsk.js"),__vite__mapDeps([433,1])),meta:{y:"p",t:"태그: otp",I:!1}}],["/tag/screct/",{loader:()=>u(()=>import("./index.html-N1Uh3b2r.js"),__vite__mapDeps([454,1])),meta:{y:"p",t:"태그: screct",I:!1}}],["/tag/pki/",{loader:()=>u(()=>import("./index.html-vsoKrW4I.js"),__vite__mapDeps([432,1])),meta:{y:"p",t:"태그: pki",I:!1}}],["/tag/mtls/",{loader:()=>u(()=>import("./index.html-CAl6qWlD.js"),__vite__mapDeps([455,1])),meta:{y:"p",t:"태그: mTLS",I:!1}}],["/tag/nomad/",{loader:()=>u(()=>import("./index.html-B8KgkMBY.js"),__vite__mapDeps([396,1])),meta:{y:"p",t:"태그: nomad",I:!1}}],["/tag/db/",{loader:()=>u(()=>import("./index.html-DtaXUAgd.js"),__vite__mapDeps([456,1])),meta:{y:"p",t:"태그: db",I:!1}}],["/tag/sentinel/",{loader:()=>u(()=>import("./index.html-B7305d4c.js"),__vite__mapDeps([457,1])),meta:{y:"p",t:"태그: sentinel",I:!1}}],["/tag/cidr/",{loader:()=>u(()=>import("./index.html-A4lvm2-S.js"),__vite__mapDeps([458,1])),meta:{y:"p",t:"태그: cidr",I:!1}}],["/tag/enterprise/",{loader:()=>u(()=>import("./index.html-S9AQkxl9.js"),__vite__mapDeps([400,1])),meta:{y:"p",t:"태그: enterprise",I:!1}}],["/tag/java/",{loader:()=>u(()=>import("./index.html-C70xfGpM.js"),__vite__mapDeps([459,1])),meta:{y:"p",t:"태그: java",I:!1}}],["/tag/spring/",{loader:()=>u(()=>import("./index.html-BpwaBgMD.js"),__vite__mapDeps([460,1])),meta:{y:"p",t:"태그: spring",I:!1}}],["/tag/performance/",{loader:()=>u(()=>import("./index.html-BMS8nkPy.js"),__vite__mapDeps([461,1])),meta:{y:"p",t:"태그: performance",I:!1}}],["/tag/vso/",{loader:()=>u(()=>import("./index.html-3ckqtICA.js"),__vite__mapDeps([462,1])),meta:{y:"p",t:"태그: VSO",I:!1}}],["/tag/windows/",{loader:()=>u(()=>import("./index.html-a4LJruRs.js"),__vite__mapDeps([463,1])),meta:{y:"p",t:"태그: windows",I:!1}}],["/tag/error/",{loader:()=>u(()=>import("./index.html-DMaoqUQj.js"),__vite__mapDeps([464,1])),meta:{y:"p",t:"태그: error",I:!1}}],["/tag/400/",{loader:()=>u(()=>import("./index.html-BbHtWqfw.js"),__vite__mapDeps([465,1])),meta:{y:"p",t:"태그: 400",I:!1}}],["/tag/miriadb/",{loader:()=>u(()=>import("./index.html-Bj13pYdT.js"),__vite__mapDeps([466,1])),meta:{y:"p",t:"태그: MiriaDB",I:!1}}],["/tag/vault/",{loader:()=>u(()=>import("./index.html-SJgMEq6R.js"),__vite__mapDeps([424,1])),meta:{y:"p",t:"태그: Vault",I:!1}}],["/tag/https/",{loader:()=>u(()=>import("./index.html-BCYYA0EV.js"),__vite__mapDeps([467,1])),meta:{y:"p",t:"태그: https",I:!1}}],["/tag/agent/",{loader:()=>u(()=>import("./index.html-D7s7Bm-s.js"),__vite__mapDeps([468,1])),meta:{y:"p",t:"태그: Agent",I:!1}}],["/tag/license/",{loader:()=>u(()=>import("./index.html-DNV1VoOS.js"),__vite__mapDeps([469,1])),meta:{y:"p",t:"태그: License",I:!1}}],["/tag/sentinel/",{loader:()=>u(()=>import("./index.html-B7305d4c.js"),__vite__mapDeps([457,1])),meta:{y:"p",t:"태그: Sentinel",I:!1}}],["/tag/cloudwatch/",{loader:()=>u(()=>import("./index.html-2OnomJdQ.js"),__vite__mapDeps([470,1])),meta:{y:"p",t:"태그: Cloudwatch",I:!1}}],["/tag/log/",{loader:()=>u(()=>import("./index.html-D5GZe-Dc.js"),__vite__mapDeps([471,1])),meta:{y:"p",t:"태그: log",I:!1}}],["/tag/namespace/",{loader:()=>u(()=>import("./index.html-BXlsA6SX.js"),__vite__mapDeps([472,1])),meta:{y:"p",t:"태그: Namespace",I:!1}}],["/tag/acl/",{loader:()=>u(()=>import("./index.html-CeLFvJF4.js"),__vite__mapDeps([409,1])),meta:{y:"p",t:"태그: ACL",I:!1}}],["/tag/ssl/",{loader:()=>u(()=>import("./index.html-Bwx_667r.js"),__vite__mapDeps([473,1])),meta:{y:"p",t:"태그: SSL",I:!1}}],["/tag/config/",{loader:()=>u(()=>import("./index.html-ChG7pV1C.js"),__vite__mapDeps([423,1])),meta:{y:"p",t:"태그: config",I:!1}}],["/tag/csi/",{loader:()=>u(()=>import("./index.html-S8M0EGI4.js"),__vite__mapDeps([398,1])),meta:{y:"p",t:"태그: csi",I:!1}}],["/tag/nfs/",{loader:()=>u(()=>import("./index.html-C9vPJFCM.js"),__vite__mapDeps([474,1])),meta:{y:"p",t:"태그: nfs",I:!1}}],["/tag/ui/",{loader:()=>u(()=>import("./index.html-DJpbJtkA.js"),__vite__mapDeps([475,1])),meta:{y:"p",t:"태그: UI",I:!1}}],["/tag/windows/",{loader:()=>u(()=>import("./index.html-a4LJruRs.js"),__vite__mapDeps([463,1])),meta:{y:"p",t:"태그: Windows",I:!1}}],["/tag/jenkins/",{loader:()=>u(()=>import("./index.html-V4RqHKzL.js"),__vite__mapDeps([452,1])),meta:{y:"p",t:"태그: Jenkins",I:!1}}],["/tag/java/",{loader:()=>u(()=>import("./index.html-C70xfGpM.js"),__vite__mapDeps([459,1])),meta:{y:"p",t:"태그: Java",I:!1}}],["/tag/docker/",{loader:()=>u(()=>import("./index.html-pQy4XdCb.js"),__vite__mapDeps([339,1])),meta:{y:"p",t:"태그: Docker",I:!1}}],["/tag/api/",{loader:()=>u(()=>import("./index.html-D_WXlAcx.js"),__vite__mapDeps([476,1])),meta:{y:"p",t:"태그: API",I:!1}}],["/tag/springboot/",{loader:()=>u(()=>import("./index.html-CfUgAf1s.js"),__vite__mapDeps([477,1])),meta:{y:"p",t:"태그: SpringBoot",I:!1}}],["/tag/sample/",{loader:()=>u(()=>import("./index.html-CdmsQh5K.js"),__vite__mapDeps([379,1])),meta:{y:"p",t:"태그: sample",I:!1}}],["/tag/job/",{loader:()=>u(()=>import("./index.html-Bt6R3NDy.js"),__vite__mapDeps([478,1])),meta:{y:"p",t:"태그: job",I:!1}}],["/tag/autoscaler/",{loader:()=>u(()=>import("./index.html-B4CBGKba.js"),__vite__mapDeps([479,1])),meta:{y:"p",t:"태그: autoscaler",I:!1}}],["/tag/das/",{loader:()=>u(()=>import("./index.html-DVPHr-AB.js"),__vite__mapDeps([480,1])),meta:{y:"p",t:"태그: das",I:!1}}],["/tag/job/",{loader:()=>u(()=>import("./index.html-Bt6R3NDy.js"),__vite__mapDeps([478,1])),meta:{y:"p",t:"태그: Job",I:!1}}],["/tag/swlb/",{loader:()=>u(()=>import("./index.html-BruheZiQ.js"),__vite__mapDeps([481,1])),meta:{y:"p",t:"태그: SWLB",I:!1}}],["/tag/vs-code/",{loader:()=>u(()=>import("./index.html-BaS3IBWL.js"),__vite__mapDeps([482,1])),meta:{y:"p",t:"태그: vs-code",I:!1}}],["/tag//",{loader:()=>u(()=>import("./index.html-zOoKtAv4.js"),__vite__mapDeps([331,1])),meta:{y:"p",t:"태그: ",I:!1}}],["/tag/ansible/",{loader:()=>u(()=>import("./index.html-BLASnx2N.js"),__vite__mapDeps([483,1])),meta:{y:"p",t:"태그: Ansible",I:!1}}],["/tag/wildfly/",{loader:()=>u(()=>import("./index.html-DAUv74io.js"),__vite__mapDeps([484,1])),meta:{y:"p",t:"태그: wildfly",I:!1}}],["/tag/jboss/",{loader:()=>u(()=>import("./index.html-CmlIA75P.js"),__vite__mapDeps([345,1])),meta:{y:"p",t:"태그: JBoss",I:!1}}],["/tag/reverse-proxy/",{loader:()=>u(()=>import("./index.html-BwINt9yw.js"),__vite__mapDeps([485,1])),meta:{y:"p",t:"태그: reverse proxy",I:!1}}],["/tag/consul-service-discovery/",{loader:()=>u(()=>import("./index.html-CJmOF4Uc.js"),__vite__mapDeps([486,1])),meta:{y:"p",t:"태그: consul service discovery",I:!1}}],["/tag/nomad-pack/",{loader:()=>u(()=>import("./index.html-B6fsItCj.js"),__vite__mapDeps([487,1])),meta:{y:"p",t:"태그: nomad-pack",I:!1}}],["/tag/vuepress/",{loader:()=>u(()=>import("./index.html-m4NvGwI9.js"),__vite__mapDeps([488,1])),meta:{y:"p",t:"태그: vuepress",I:!1}}],["/tag/param/",{loader:()=>u(()=>import("./index.html-CN16WffQ.js"),__vite__mapDeps([489,1])),meta:{y:"p",t:"태그: param",I:!1}}],["/tag/batch/",{loader:()=>u(()=>import("./index.html-BluWWvPM.js"),__vite__mapDeps([490,1])),meta:{y:"p",t:"태그: batch",I:!1}}],["/tag/scouter/",{loader:()=>u(()=>import("./index.html-ZxzMFLB7.js"),__vite__mapDeps([491,1])),meta:{y:"p",t:"태그: Scouter",I:!1}}],["/tag/service-mesh/",{loader:()=>u(()=>import("./index.html-qlIMCSXC.js"),__vite__mapDeps([492,1])),meta:{y:"p",t:"태그: Service Mesh",I:!1}}],["/tag/sidecar/",{loader:()=>u(()=>import("./index.html-B4LLQU-B.js"),__vite__mapDeps([419,1])),meta:{y:"p",t:"태그: sidecar",I:!1}}],["/tag/tomcat/",{loader:()=>u(()=>import("./index.html-D7p9qpLi.js"),__vite__mapDeps([493,1])),meta:{y:"p",t:"태그: tomcat",I:!1}}],["/tag/hashicorp/",{loader:()=>u(()=>import("./index.html-Cfko_1rJ.js"),__vite__mapDeps([335,1])),meta:{y:"p",t:"태그: Hashicorp",I:!1}}],["/tag/update/",{loader:()=>u(()=>import("./index.html-C_mWrlPz.js"),__vite__mapDeps([494,1])),meta:{y:"p",t:"태그: Update",I:!1}}],["/tag/jan/",{loader:()=>u(()=>import("./index.html-D7-__zu2.js"),__vite__mapDeps([495,1])),meta:{y:"p",t:"태그: Jan",I:!1}}],["/tag/feb/",{loader:()=>u(()=>import("./index.html-BvrB7I3s.js"),__vite__mapDeps([496,1])),meta:{y:"p",t:"태그: Feb",I:!1}}],["/tag/mar/",{loader:()=>u(()=>import("./index.html-Nzf5cVbT.js"),__vite__mapDeps([497,1])),meta:{y:"p",t:"태그: Mar",I:!1}}],["/tag/apr/",{loader:()=>u(()=>import("./index.html-Zsxrrt81.js"),__vite__mapDeps([498,1])),meta:{y:"p",t:"태그: Apr",I:!1}}],["/tag/may/",{loader:()=>u(()=>import("./index.html-C3iva97a.js"),__vite__mapDeps([499,1])),meta:{y:"p",t:"태그: May",I:!1}}],["/tag/jun/",{loader:()=>u(()=>import("./index.html-C1NKfsey.js"),__vite__mapDeps([500,1])),meta:{y:"p",t:"태그: Jun",I:!1}}],["/tag/jul/",{loader:()=>u(()=>import("./index.html-BwBRLpAS.js"),__vite__mapDeps([501,1])),meta:{y:"p",t:"태그: Jul",I:!1}}],["/tag/aug/",{loader:()=>u(()=>import("./index.html-Ck3tx8bR.js"),__vite__mapDeps([502,1])),meta:{y:"p",t:"태그: Aug",I:!1}}],["/tag/sep/",{loader:()=>u(()=>import("./index.html-BhcS2u_z.js"),__vite__mapDeps([503,1])),meta:{y:"p",t:"태그: Sep",I:!1}}],["/tag/oct/",{loader:()=>u(()=>import("./index.html-DrU5Zw3c.js"),__vite__mapDeps([504,1])),meta:{y:"p",t:"태그: Oct",I:!1}}],["/tag/nov/",{loader:()=>u(()=>import("./index.html-DxLgIHet.js"),__vite__mapDeps([505,1])),meta:{y:"p",t:"태그: Nov",I:!1}}],["/tag/july/",{loader:()=>u(()=>import("./index.html-C2yf9D51.js"),__vite__mapDeps([506,1])),meta:{y:"p",t:"태그: July",I:!1}}],["/tag/dec/",{loader:()=>u(()=>import("./index.html-B6kMSFtp.js"),__vite__mapDeps([507,1])),meta:{y:"p",t:"태그: Dec",I:!1}}],["/tag/cicd/",{loader:()=>u(()=>import("./index.html-CX5BgQPA.js"),__vite__mapDeps([508,1])),meta:{y:"p",t:"태그: cicd",I:!1}}],["/tag/tomcat/",{loader:()=>u(()=>import("./index.html-D7p9qpLi.js"),__vite__mapDeps([493,1])),meta:{y:"p",t:"태그: Tomcat",I:!1}}],["/tag/ncloud/",{loader:()=>u(()=>import("./index.html-BJFe6ZiL.js"),__vite__mapDeps([509,1])),meta:{y:"p",t:"태그: ncloud",I:!1}}],["/tag/ncp/",{loader:()=>u(()=>import("./index.html-BF9sdaEF.js"),__vite__mapDeps([383,1])),meta:{y:"p",t:"태그: ncp",I:!1}}],["/tag/workshop/",{loader:()=>u(()=>import("./index.html-odQGV5Lk.js"),__vite__mapDeps([510,1])),meta:{y:"p",t:"태그: workshop",I:!1}}],["/tag/k8s/",{loader:()=>u(()=>import("./index.html-CikwTvHf.js"),__vite__mapDeps([416,1])),meta:{y:"p",t:"태그: K8s",I:!1}}],["/tag/ingress/",{loader:()=>u(()=>import("./index.html-CXv2rEdZ.js"),__vite__mapDeps([511,1])),meta:{y:"p",t:"태그: ingress",I:!1}}],["/tag/annotation/",{loader:()=>u(()=>import("./index.html-bgIO2JEg.js"),__vite__mapDeps([512,1])),meta:{y:"p",t:"태그: annotation",I:!1}}],["/tag/timeout/",{loader:()=>u(()=>import("./index.html-ChgtSZ-Z.js"),__vite__mapDeps([513,1])),meta:{y:"p",t:"태그: timeout",I:!1}}],["/tag/istio/",{loader:()=>u(()=>import("./index.html-CHfJCgT3.js"),__vite__mapDeps([514,1])),meta:{y:"p",t:"태그: Istio",I:!1}}],["/tag/performance/",{loader:()=>u(()=>import("./index.html-BMS8nkPy.js"),__vite__mapDeps([461,1])),meta:{y:"p",t:"태그: Performance",I:!1}}],["/tag/jaeger/",{loader:()=>u(()=>import("./index.html-Cij7iqH7.js"),__vite__mapDeps([515,1])),meta:{y:"p",t:"태그: Jaeger",I:!1}}],["/tag/tracing/",{loader:()=>u(()=>import("./index.html-CoUmmtwf.js"),__vite__mapDeps([516,1])),meta:{y:"p",t:"태그: Tracing",I:!1}}],["/tag/opentelemetry/",{loader:()=>u(()=>import("./index.html-DEE8lR3g.js"),__vite__mapDeps([517,1])),meta:{y:"p",t:"태그: OpenTelemetry",I:!1}}],["/tag/ingressgateway/",{loader:()=>u(()=>import("./index.html-B-AYcfLz.js"),__vite__mapDeps([518,1])),meta:{y:"p",t:"태그: IngressGateway",I:!1}}],["/tag/operator/",{loader:()=>u(()=>import("./index.html-BN6tS7qC.js"),__vite__mapDeps([519,1])),meta:{y:"p",t:"태그: operator",I:!1}}],["/article/",{loader:()=>u(()=>import("./index.html-BNePmBVJ.js"),__vite__mapDeps([520,1])),meta:{y:"p",t:"게시글",I:!1}}],["/star/",{loader:()=>u(()=>import("./index.html-CeSawCUo.js"),__vite__mapDeps([521,1])),meta:{y:"p",t:"스타",I:!1}}],["/timeline/",{loader:()=>u(()=>import("./index.html-cSXKi2Z5.js"),__vite__mapDeps([522,1])),meta:{y:"p",t:"타임라인",I:!1}}]]);/*! + * vue-router v4.2.5 + * (c) 2023 Eduardo San Martin Morote + * @license MIT + */const xt=typeof window<"u";function jm(e){return e.__esModule||e[Symbol.toStringTag]==="Module"}const fe=Object.assign;function Zs(e,n){const t={};for(const a in n){const s=n[a];t[a]=bn(s)?s.map(e):e(s)}return t}const pa=()=>{},bn=Array.isArray,Mm=/\/$/,$m=e=>e.replace(Mm,"");function el(e,n,t="/"){let a,s={},l="",o="";const r=n.indexOf("#");let c=n.indexOf("?");return r=0&&(c=-1),c>-1&&(a=n.slice(0,c),l=n.slice(c+1,r>-1?r:n.length),s=e(l)),r>-1&&(a=a||n.slice(0,r),o=n.slice(r,n.length)),a=Km(a??n,t),{fullPath:a+(l&&"?")+l+o,path:a,query:s,hash:o}}function Bm(e,n){const t=n.query?e(n.query):"";return n.path+(t&&"?")+t+(n.hash||"")}function Or(e,n){return!n||!e.toLowerCase().startsWith(n.toLowerCase())?e:e.slice(n.length)||"/"}function Um(e,n,t){const a=n.matched.length-1,s=t.matched.length-1;return a>-1&&a===s&&Mt(n.matched[a],t.matched[s])&&Hp(n.params,t.params)&&e(n.query)===e(t.query)&&n.hash===t.hash}function Mt(e,n){return(e.aliasOf||e)===(n.aliasOf||n)}function Hp(e,n){if(Object.keys(e).length!==Object.keys(n).length)return!1;for(const t in e)if(!zm(e[t],n[t]))return!1;return!0}function zm(e,n){return bn(e)?Dr(e,n):bn(n)?Dr(n,e):e===n}function Dr(e,n){return bn(n)?e.length===n.length&&e.every((t,a)=>t===n[a]):e.length===1&&e[0]===n}function Km(e,n){if(e.startsWith("/"))return e;if(!e)return n;const t=n.split("/"),a=e.split("/"),s=a[a.length-1];(s===".."||s===".")&&a.push("");let l=t.length-1,o,r;for(o=0;o 1&&l--;else break;return t.slice(0,l).join("/")+"/"+a.slice(o-(o===a.length?1:0)).join("/")}var ba;(function(e){e.pop="pop",e.push="push"})(ba||(ba={}));var ca;(function(e){e.back="back",e.forward="forward",e.unknown=""})(ca||(ca={}));function qm(e){if(!e)if(xt){const n=document.querySelector("base");e=n&&n.getAttribute("href")||"/",e=e.replace(/^\w+:\/\/[^\/]+/,"")}else e="/";return e[0]!=="/"&&e[0]!=="#"&&(e="/"+e),$m(e)}const Jm=/^[^#]+#/;function Wm(e,n){return e.replace(Jm,"#")+n}function Gm(e,n){const t=document.documentElement.getBoundingClientRect(),a=e.getBoundingClientRect();return{behavior:n.behavior,left:a.left-t.left-(n.left||0),top:a.top-t.top-(n.top||0)}}const Ps=()=>({left:window.pageXOffset,top:window.pageYOffset});function Ym(e){let n;if("el"in e){const t=e.el,a=typeof t=="string"&&t.startsWith("#"),s=typeof t=="string"?a?document.getElementById(t.slice(1)):document.querySelector(t):t;if(!s)return;n=Gm(s,e)}else n=e;"scrollBehavior"in document.documentElement.style?window.scrollTo(n):window.scrollTo(n.left!=null?n.left:window.pageXOffset,n.top!=null?n.top:window.pageYOffset)}function Rr(e,n){return(history.state?history.state.position-n:-1)+e}const xl=new Map;function Xm(e,n){xl.set(e,n)}function Qm(e){const n=xl.get(e);return xl.delete(e),n}let Zm=()=>location.protocol+"//"+location.host;function Fp(e,n){const{pathname:t,search:a,hash:s}=n,l=e.indexOf("#");if(l>-1){let r=s.includes(e.slice(l))?e.slice(l).length:1,c=s.slice(r);return c[0]!=="/"&&(c="/"+c),Or(c,"")}return Or(t,e)+a+s}function e0(e,n,t,a){let s=[],l=[],o=null;const r=({state:m})=>{const g=Fp(e,location),v=t.value,w=n.value;let C=0;if(m){if(t.value=g,n.value=m,o&&o===v){o=null;return}C=w?m.position-w.position:0}else a(g);s.forEach(_=>{_(t.value,v,{delta:C,type:ba.pop,direction:C?C>0?ca.forward:ca.back:ca.unknown})})};function c(){o=t.value}function p(m){s.push(m);const g=()=>{const v=s.indexOf(m);v>-1&&s.splice(v,1)};return l.push(g),g}function d(){const{history:m}=window;m.state&&m.replaceState(fe({},m.state,{scroll:Ps()}),"")}function h(){for(const m of l)m();l=[],window.removeEventListener("popstate",r),window.removeEventListener("beforeunload",d)}return window.addEventListener("popstate",r),window.addEventListener("beforeunload",d,{passive:!0}),{pauseListeners:c,listen:p,destroy:h}}function Hr(e,n,t,a=!1,s=!1){return{back:e,current:n,forward:t,replaced:a,position:window.history.length,scroll:s?Ps():null}}function n0(e){const{history:n,location:t}=window,a={value:Fp(e,t)},s={value:n.state};s.value||l(a.value,{back:null,current:a.value,forward:null,position:n.length-1,replaced:!0,scroll:null},!0);function l(c,p,d){const h=e.indexOf("#"),m=h>-1?(t.host&&document.querySelector("base")?e:e.slice(h))+c:Zm()+e+c;try{n[d?"replaceState":"pushState"](p,"",m),s.value=p}catch(g){console.error(g),t[d?"replace":"assign"](m)}}function o(c,p){const d=fe({},n.state,Hr(s.value.back,c,s.value.forward,!0),p,{position:s.value.position});l(c,d,!0),a.value=c}function r(c,p){const d=fe({},s.value,n.state,{forward:c,scroll:Ps()});l(d.current,d,!0);const h=fe({},Hr(a.value,c,null),{position:d.position+1},p);l(c,h,!1),a.value=c}return{location:a,state:s,push:r,replace:o}}function t0(e){e=qm(e);const n=n0(e),t=e0(e,n.state,n.location,n.replace);function a(l,o=!0){o||t.pauseListeners(),history.go(l)}const s=fe({location:"",base:e,go:a,createHref:Wm.bind(null,e)},n,t);return Object.defineProperty(s,"location",{enumerable:!0,get:()=>n.location.value}),Object.defineProperty(s,"state",{enumerable:!0,get:()=>n.state.value}),s}function a0(e){return typeof e=="string"||e&&typeof e=="object"}function Np(e){return typeof e=="string"||typeof e=="symbol"}const Rn={path:"/",name:void 0,params:{},query:{},hash:"",fullPath:"/",matched:[],meta:{},redirectedFrom:void 0},jp=Symbol("");var Fr;(function(e){e[e.aborted=4]="aborted",e[e.cancelled=8]="cancelled",e[e.duplicated=16]="duplicated"})(Fr||(Fr={}));function $t(e,n){return fe(new Error,{type:e,[jp]:!0},n)}function On(e,n){return e instanceof Error&&jp in e&&(n==null||!!(e.type&n))}const Nr="[^/]+?",s0={sensitive:!1,strict:!1,start:!0,end:!0},l0=/[.+*?^${}()[\]/\\]/g;function o0(e,n){const t=fe({},s0,n),a=[];let s=t.start?"^":"";const l=[];for(const p of e){const d=p.length?[]:[90];t.strict&&!p.length&&(s+="/");for(let h=0;h n.length?n.length===1&&n[0]===80?1:-1:0}function i0(e,n){let t=0;const a=e.score,s=n.score;for(;t 0&&n[n.length-1]<0}const p0={type:0,value:""},c0=/[a-zA-Z0-9_]/;function u0(e){if(!e)return[[]];if(e==="/")return[[p0]];if(!e.startsWith("/"))throw new Error(`Invalid path "${e}"`);function n(g){throw new Error(`ERR (${t})/"${p}": ${g}`)}let t=0,a=t;const s=[];let l;function o(){l&&s.push(l),l=[]}let r=0,c,p="",d="";function h(){p&&(t===0?l.push({type:0,value:p}):t===1||t===2||t===3?(l.length>1&&(c==="*"||c==="+")&&n(`A repeatable param (${p}) must be alone in its segment. eg: '/:ids+.`),l.push({type:1,value:p,regexp:d,repeatable:c==="*"||c==="+",optional:c==="*"||c==="?"})):n("Invalid state to consume buffer"),p="")}function m(){p+=c}for(;r {o(T)}:pa}function o(d){if(Np(d)){const h=a.get(d);h&&(a.delete(d),t.splice(t.indexOf(h),1),h.children.forEach(o),h.alias.forEach(o))}else{const h=t.indexOf(d);h>-1&&(t.splice(h,1),d.record.name&&a.delete(d.record.name),d.children.forEach(o),d.alias.forEach(o))}}function r(){return t}function c(d){let h=0;for(;h =0&&(d.record.path!==t[h].record.path||!Mp(d,t[h]));)h++;t.splice(h,0,d),d.record.name&&!$r(d)&&a.set(d.record.name,d)}function p(d,h){let m,g={},v,w;if("name"in d&&d.name){if(m=a.get(d.name),!m)throw $t(1,{location:d});w=m.record.name,g=fe(Mr(h.params,m.keys.filter(T=>!T.optional).map(T=>T.name)),d.params&&Mr(d.params,m.keys.map(T=>T.name))),v=m.stringify(g)}else if("path"in d)v=d.path,m=t.find(T=>T.re.test(v)),m&&(g=m.parse(v),w=m.record.name);else{if(m=h.name?a.get(h.name):t.find(T=>T.re.test(h.path)),!m)throw $t(1,{location:d,currentLocation:h});w=m.record.name,g=fe({},h.params,d.params),v=m.stringify(g)}const C=[];let _=m;for(;_;)C.unshift(_.record),_=_.parent;return{name:w,path:v,params:g,matched:C,meta:k0(C)}}return e.forEach(d=>l(d)),{addRoute:l,resolve:p,removeRoute:o,getRoutes:r,getRecordMatcher:s}}function Mr(e,n){const t={};for(const a of n)a in e&&(t[a]=e[a]);return t}function m0(e){return{path:e.path,redirect:e.redirect,name:e.name,meta:e.meta||{},aliasOf:void 0,beforeEnter:e.beforeEnter,props:g0(e),children:e.children||[],instances:{},leaveGuards:new Set,updateGuards:new Set,enterCallbacks:{},components:"components"in e?e.components||null:e.component&&{default:e.component}}}function g0(e){const n={},t=e.props||!1;if("component"in e)n.default=t;else for(const a in e.components)n[a]=typeof t=="object"?t[a]:t;return n}function $r(e){for(;e;){if(e.record.aliasOf)return!0;e=e.parent}return!1}function k0(e){return e.reduce((n,t)=>fe(n,t.meta),{})}function Br(e,n){const t={};for(const a in e)t[a]=a in n?n[a]:e[a];return t}function Mp(e,n){return n.children.some(t=>t===e||Mp(e,t))}const $p=/#/g,f0=/&/g,v0=/\//g,_0=/=/g,b0=/\?/g,Bp=/\+/g,y0=/%5B/g,w0=/%5D/g,Up=/%5E/g,C0=/%60/g,zp=/%7B/g,E0=/%7C/g,Kp=/%7D/g,x0=/%20/g;function po(e){return encodeURI(""+e).replace(E0,"|").replace(y0,"[").replace(w0,"]")}function T0(e){return po(e).replace(zp,"{").replace(Kp,"}").replace(Up,"^")}function Tl(e){return po(e).replace(Bp,"%2B").replace(x0,"+").replace($p,"%23").replace(f0,"%26").replace(C0,"`").replace(zp,"{").replace(Kp,"}").replace(Up,"^")}function S0(e){return Tl(e).replace(_0,"%3D")}function L0(e){return po(e).replace($p,"%23").replace(b0,"%3F")}function P0(e){return e==null?"":L0(e).replace(v0,"%2F")}function gs(e){try{return decodeURIComponent(""+e)}catch{}return""+e}function I0(e){const n={};if(e===""||e==="?")return n;const a=(e[0]==="?"?e.slice(1):e).split("&");for(let s=0;s l&&Tl(l)):[a&&Tl(a)]).forEach(l=>{l!==void 0&&(n+=(n.length?"&":"")+t,l!=null&&(n+="="+l))})}return n}function A0(e){const n={};for(const t in e){const a=e[t];a!==void 0&&(n[t]=bn(a)?a.map(s=>s==null?null:""+s):a==null?a:""+a)}return n}const V0=Symbol(""),zr=Symbol(""),Is=Symbol(""),co=Symbol(""),Sl=Symbol("");function ea(){let e=[];function n(a){return e.push(a),()=>{const s=e.indexOf(a);s>-1&&e.splice(s,1)}}function t(){e=[]}return{add:n,list:()=>e.slice(),reset:t}}function Yn(e,n,t,a,s){const l=a&&(a.enterCallbacks[s]=a.enterCallbacks[s]||[]);return()=>new Promise((o,r)=>{const c=h=>{h===!1?r($t(4,{from:t,to:n})):h instanceof Error?r(h):a0(h)?r($t(2,{from:n,to:h})):(l&&a.enterCallbacks[s]===l&&typeof h=="function"&&l.push(h),o())},p=e.call(a&&a.instances[s],n,t,c);let d=Promise.resolve(p);e.length<3&&(d=d.then(c)),d.catch(h=>r(h))})}function nl(e,n,t,a){const s=[];for(const l of e)for(const o in l.components){let r=l.components[o];if(!(n!=="beforeRouteEnter"&&!l.instances[o]))if(O0(r)){const p=(r.__vccOpts||r)[n];p&&s.push(Yn(p,t,a,l,o))}else{let c=r();s.push(()=>c.then(p=>{if(!p)return Promise.reject(new Error(`Couldn't resolve component "${o}" at "${l.path}"`));const d=jm(p)?p.default:p;l.components[o]=d;const m=(d.__vccOpts||d)[n];return m&&Yn(m,t,a,l,o)()}))}}return s}function O0(e){return typeof e=="object"||"displayName"in e||"props"in e||"__vccOpts"in e}function Kr(e){const n=be(Is),t=be(co),a=b(()=>n.resolve(fn(e.to))),s=b(()=>{const{matched:c}=a.value,{length:p}=c,d=c[p-1],h=t.matched;if(!d||!h.length)return-1;const m=h.findIndex(Mt.bind(null,d));if(m>-1)return m;const g=qr(c[p-2]);return p>1&&qr(d)===g&&h[h.length-1].path!==g?h.findIndex(Mt.bind(null,c[p-2])):m}),l=b(()=>s.value>-1&&F0(t.params,a.value.params)),o=b(()=>s.value>-1&&s.value===t.matched.length-1&&Hp(t.params,a.value.params));function r(c={}){return H0(c)?n[fn(e.replace)?"replace":"push"](fn(e.to)).catch(pa):Promise.resolve()}return{route:a,href:b(()=>a.value.href),isActive:l,isExactActive:o,navigate:r}}const D0=V({name:"RouterLink",compatConfig:{MODE:3},props:{to:{type:[String,Object],required:!0},replace:Boolean,activeClass:String,exactActiveClass:String,custom:Boolean,ariaCurrentValue:{type:String,default:"page"}},useLink:Kr,setup(e,{slots:n}){const t=xa(Kr(e)),{options:a}=be(Is),s=b(()=>({[Jr(e.activeClass,a.linkActiveClass,"router-link-active")]:t.isActive,[Jr(e.exactActiveClass,a.linkExactActiveClass,"router-link-exact-active")]:t.isExactActive}));return()=>{const l=n.default&&n.default(t);return e.custom?l:i("a",{"aria-current":t.isExactActive?e.ariaCurrentValue:null,href:t.href,onClick:t.navigate,class:s.value},l)}}}),R0=D0;function H0(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&!(e.button!==void 0&&e.button!==0)){if(e.currentTarget&&e.currentTarget.getAttribute){const n=e.currentTarget.getAttribute("target");if(/\b_blank\b/i.test(n))return}return e.preventDefault&&e.preventDefault(),!0}}function F0(e,n){for(const t in n){const a=n[t],s=e[t];if(typeof a=="string"){if(a!==s)return!1}else if(!bn(s)||s.length!==a.length||a.some((l,o)=>l!==s[o]))return!1}return!0}function qr(e){return e?e.aliasOf?e.aliasOf.path:e.path:""}const Jr=(e,n,t)=>e??n??t,N0=V({name:"RouterView",inheritAttrs:!1,props:{name:{type:String,default:"default"},route:Object},compatConfig:{MODE:3},setup(e,{attrs:n,slots:t}){const a=be(Sl),s=b(()=>e.route||a.value),l=be(zr,0),o=b(()=>{let p=fn(l);const{matched:d}=s.value;let h;for(;(h=d[p])&&!h.components;)p++;return p}),r=b(()=>s.value.matched[o.value]);hn(zr,b(()=>o.value+1)),hn(V0,r),hn(Sl,s);const c=q();return oe(()=>[c.value,r.value,e.name],([p,d,h],[m,g,v])=>{d&&(d.instances[h]=p,g&&g!==d&&p&&p===m&&(d.leaveGuards.size||(d.leaveGuards=g.leaveGuards),d.updateGuards.size||(d.updateGuards=g.updateGuards))),p&&d&&(!g||!Mt(d,g)||!m)&&(d.enterCallbacks[h]||[]).forEach(w=>w(p))},{flush:"post"}),()=>{const p=s.value,d=e.name,h=r.value,m=h&&h.components[d];if(!m)return Wr(t.default,{Component:m,route:p});const g=h.props[d],v=g?g===!0?p.params:typeof g=="function"?g(p):g:null,C=i(m,fe({},v,n,{onVnodeUnmounted:_=>{_.component.isUnmounted&&(h.instances[d]=null)},ref:c}));return Wr(t.default,{Component:C,route:p})||C}}});function Wr(e,n){if(!e)return null;const t=e(n);return t.length===1?t[0]:t}const j0=N0;function M0(e){const n=h0(e.routes,e),t=e.parseQuery||I0,a=e.stringifyQuery||Ur,s=e.history,l=ea(),o=ea(),r=ea(),c=ge(Rn);let p=Rn;xt&&e.scrollBehavior&&"scrollRestoration"in history&&(history.scrollRestoration="manual");const d=Zs.bind(null,L=>""+L),h=Zs.bind(null,P0),m=Zs.bind(null,gs);function g(L,U){let $,G;return Np(L)?($=n.getRecordMatcher(L),G=U):G=L,n.addRoute(G,$)}function v(L){const U=n.getRecordMatcher(L);U&&n.removeRoute(U)}function w(){return n.getRoutes().map(L=>L.record)}function C(L){return!!n.getRecordMatcher(L)}function _(L,U){if(U=fe({},U||c.value),typeof L=="string"){const f=el(t,L,U.path),E=n.resolve({path:f.path},U),I=s.createHref(f.fullPath);return fe(f,E,{params:m(E.params),hash:gs(f.hash),redirectedFrom:void 0,href:I})}let $;if("path"in L)$=fe({},L,{path:el(t,L.path,U.path).path});else{const f=fe({},L.params);for(const E in f)f[E]==null&&delete f[E];$=fe({},L,{params:h(f)}),U.params=h(U.params)}const G=n.resolve($,U),pe=L.hash||"";G.params=d(m(G.params));const we=Bm(a,fe({},L,{hash:T0(pe),path:G.path})),k=s.createHref(we);return fe({fullPath:we,hash:pe,query:a===Ur?A0(L.query):L.query||{}},G,{redirectedFrom:void 0,href:k})}function T(L){return typeof L=="string"?el(t,L,c.value.path):fe({},L)}function y(L,U){if(p!==L)return $t(8,{from:U,to:L})}function S(L){return K(L)}function R(L){return S(fe(T(L),{replace:!0}))}function x(L){const U=L.matched[L.matched.length-1];if(U&&U.redirect){const{redirect:$}=U;let G=typeof $=="function"?$(L):$;return typeof G=="string"&&(G=G.includes("?")||G.includes("#")?G=T(G):{path:G},G.params={}),fe({query:L.query,hash:L.hash,params:"path"in G?{}:L.params},G)}}function K(L,U){const $=p=_(L),G=c.value,pe=L.state,we=L.force,k=L.replace===!0,f=x($);if(f)return K(fe(T(f),{state:typeof f=="object"?fe({},pe,f.state):pe,force:we,replace:k}),U||$);const E=$;E.redirectedFrom=U;let I;return!we&&Um(a,G,$)&&(I=$t(16,{to:E,from:G}),on(G,G,!0,!1)),(I?Promise.resolve(I):j(E,G)).catch(P=>On(P)?On(P,2)?P:wn(P):W(P,E,G)).then(P=>{if(P){if(On(P,2))return K(fe({replace:k},T(P.to),{state:typeof P.to=="object"?fe({},pe,P.to.state):pe,force:we}),U||E)}else P=M(E,G,!0,k,pe);return Y(E,G,P),P})}function F(L,U){const $=y(L,U);return $?Promise.reject($):Promise.resolve()}function H(L){const U=Vn.values().next().value;return U&&typeof U.runWithContext=="function"?U.runWithContext(L):L()}function j(L,U){let $;const[G,pe,we]=$0(L,U);$=nl(G.reverse(),"beforeRouteLeave",L,U);for(const f of G)f.leaveGuards.forEach(E=>{$.push(Yn(E,L,U))});const k=F.bind(null,L,U);return $.push(k),De($).then(()=>{$=[];for(const f of l.list())$.push(Yn(f,L,U));return $.push(k),De($)}).then(()=>{$=nl(pe,"beforeRouteUpdate",L,U);for(const f of pe)f.updateGuards.forEach(E=>{$.push(Yn(E,L,U))});return $.push(k),De($)}).then(()=>{$=[];for(const f of we)if(f.beforeEnter)if(bn(f.beforeEnter))for(const E of f.beforeEnter)$.push(Yn(E,L,U));else $.push(Yn(f.beforeEnter,L,U));return $.push(k),De($)}).then(()=>(L.matched.forEach(f=>f.enterCallbacks={}),$=nl(we,"beforeRouteEnter",L,U),$.push(k),De($))).then(()=>{$=[];for(const f of o.list())$.push(Yn(f,L,U));return $.push(k),De($)}).catch(f=>On(f,8)?f:Promise.reject(f))}function Y(L,U,$){r.list().forEach(G=>H(()=>G(L,U,$)))}function M(L,U,$,G,pe){const we=y(L,U);if(we)return we;const k=U===Rn,f=xt?history.state:{};$&&(G||k?s.replace(L.fullPath,fe({scroll:k&&f&&f.scroll},pe)):s.push(L.fullPath,pe)),c.value=L,on(L,U,$,k),wn()}let ee;function Ie(){ee||(ee=s.listen((L,U,$)=>{if(!Cn.listening)return;const G=_(L),pe=x(G);if(pe){K(fe(pe,{replace:!0}),G).catch(pa);return}p=G;const we=c.value;xt&&Xm(Rr(we.fullPath,$.delta),Ps()),j(G,we).catch(k=>On(k,12)?k:On(k,2)?(K(k.to,G).then(f=>{On(f,20)&&!$.delta&&$.type===ba.pop&&s.go(-1,!1)}).catch(pa),Promise.reject()):($.delta&&s.go(-$.delta,!1),W(k,G,we))).then(k=>{k=k||M(G,we,!1),k&&($.delta&&!On(k,8)?s.go(-$.delta,!1):$.type===ba.pop&&On(k,20)&&s.go(-1,!1)),Y(G,we,k)}).catch(pa)}))}let Se=ea(),J=ea(),ne;function W(L,U,$){wn(L);const G=J.list();return G.length?G.forEach(pe=>pe(L,U,$)):console.error(L),Promise.reject(L)}function Oe(){return ne&&c.value!==Rn?Promise.resolve():new Promise((L,U)=>{Se.add([L,U])})}function wn(L){return ne||(ne=!L,Ie(),Se.list().forEach(([U,$])=>L?$(L):U()),Se.reset()),L}function on(L,U,$,G){const{scrollBehavior:pe}=e;if(!xt||!pe)return Promise.resolve();const we=!$&&Qm(Rr(L.fullPath,0))||(G||!$)&&history.state&&history.state.scroll||null;return vt().then(()=>pe(L,U,we)).then(k=>k&&Ym(k)).catch(k=>W(k,L,U))}const Ne=L=>s.go(L);let en;const Vn=new Set,Cn={currentRoute:c,listening:!0,addRoute:g,removeRoute:v,hasRoute:C,getRoutes:w,resolve:_,options:e,push:S,replace:R,go:Ne,back:()=>Ne(-1),forward:()=>Ne(1),beforeEach:l.add,beforeResolve:o.add,afterEach:r.add,onError:J.add,isReady:Oe,install(L){const U=this;L.component("RouterLink",R0),L.component("RouterView",j0),L.config.globalProperties.$router=U,Object.defineProperty(L.config.globalProperties,"$route",{enumerable:!0,get:()=>fn(c)}),xt&&!en&&c.value===Rn&&(en=!0,S(s.location).catch(pe=>{}));const $={};for(const pe in Rn)Object.defineProperty($,pe,{get:()=>c.value[pe],enumerable:!0});L.provide(Is,U),L.provide(co,Bi($)),L.provide(Sl,c);const G=L.unmount;Vn.add(L),L.unmount=function(){Vn.delete(L),Vn.size<1&&(p=Rn,ee&&ee(),ee=null,c.value=Rn,en=!1,ne=!1),G()}}};function De(L){return L.reduce((U,$)=>U.then(()=>H($)),Promise.resolve())}return Cn}function $0(e,n){const t=[],a=[],s=[],l=Math.max(n.matched.length,e.matched.length);for(let o=0;o Mt(p,r))?a.push(r):t.push(r));const c=e.matched[o];c&&(n.matched.find(p=>Mt(p,c))||s.push(c))}return[t,a,s]}function Pn(){return be(Is)}function In(){return be(co)}var uo=Symbol(""),An=()=>{const e=be(uo);if(!e)throw new Error("useClientData() is called without provider.");return e},B0=()=>An().pageComponent,ke=()=>An().pageData,ye=()=>An().pageFrontmatter,U0=()=>An().pageHead,qp=()=>An().pageLang,z0=()=>An().pageLayout,ln=()=>An().routeLocale,K0=()=>An().routes,Jp=()=>An().siteData,Ia=()=>An().siteLocaleData,q0=Symbol(""),Wp=ge(Fm),ya=ge(Nm),Gp=e=>{const n=Om(e);if(ya.value[n])return n;const t=encodeURI(n);return ya.value[t]?t:Wp.value[n]||n},Wt=e=>{const n=Gp(e),t=ya.value[n]??{...ya.value["/404.html"],notFound:!0};return{path:n,notFound:!1,...t}},As=V({name:"ClientOnly",setup(e,n){const t=q(!1);return se(()=>{t.value=!0}),()=>{var a,s;return t.value?(s=(a=n.slots).default)==null?void 0:s.call(a):null}}}),Yp=V({name:"Content",props:{path:{type:String,required:!1,default:""}},setup(e){const n=B0(),t=b(()=>{if(!e.path)return n.value;const a=Wt(e.path);return eh(()=>a.loader().then(({comp:s})=>s))});return()=>i(t.value)}}),Ce=e=>yn(e)?e:`/${Dp(e)}`,J0=e=>{if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&!(e.button!==void 0&&e.button!==0)){if(e.currentTarget){const n=e.currentTarget.getAttribute("target");if(n!=null&&n.match(/\b_blank\b/i))return}return e.preventDefault(),!0}},Ve=({active:e=!1,activeClass:n="route-link-active",to:t,...a},{slots:s})=>{var r;const l=Pn(),o=Ce(Gp(t));return i("a",{...a,class:["route-link",{[n]:e}],href:o,onClick:(c={})=>{J0(c)?l.push(t).catch():Promise.resolve()}},(r=s.default)==null?void 0:r.call(s))};Ve.displayName="RouteLink";Ve.props={active:Boolean,activeClass:String,to:String};var W0="Layout",G0="en-US",rt=xa({resolveLayouts:e=>e.reduce((n,t)=>({...n,...t.layouts}),{}),resolvePageHead:(e,n,t)=>{const a=me(n.description)?n.description:t.description,s=[...Array.isArray(n.head)?n.head:[],...t.head,["title",{},e],["meta",{name:"description",content:a}]];return Im(s)},resolvePageHeadTitle:(e,n)=>[e.title,n.title].filter(t=>!!t).join(" | "),resolvePageLang:(e,n)=>e.lang||n.lang||G0,resolvePageLayout:(e,n)=>{const t=me(e.frontmatter.layout)?e.frontmatter.layout:W0;return n[t]},resolveRouteLocale:(e,n)=>Dm(e,n),resolveSiteLocaleData:(e,n)=>{var t;return{...e,...e.locales[n],head:[...((t=e.locales[n])==null?void 0:t.head)??[],...e.head??[]]}}});const Y0={};var Ge=(e={})=>e,Je=Uint8Array,Tt=Uint16Array,X0=Int32Array,Xp=new Je([0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0]),Qp=new Je([0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0]),Q0=new Je([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),Zp=function(e,n){for(var t=new Tt(31),a=0;a<31;++a)t[a]=n+=1< >1|(Te&21845)<<1;Un=(Un&52428)>>2|(Un&13107)<<2,Un=(Un&61680)>>4|(Un&3855)<<4,Ll[Te]=((Un&65280)>>8|(Un&255)<<8)>>1}var ua=function(e,n,t){for(var a=e.length,s=0,l=new Tt(n);s>c]=p}else for(r=new Tt(a),s=0;s>15-e[s]);return r},Aa=new Je(288);for(var Te=0;Te<144;++Te)Aa[Te]=8;for(var Te=144;Te<256;++Te)Aa[Te]=9;for(var Te=256;Te<280;++Te)Aa[Te]=7;for(var Te=280;Te<288;++Te)Aa[Te]=8;var tc=new Je(32);for(var Te=0;Te<32;++Te)tc[Te]=5;var tg=ua(Aa,9,1),ag=ua(tc,5,1),tl=function(e){for(var n=e[0],t=1;t n&&(n=e[t]);return n},gn=function(e,n,t){var a=n/8|0;return(e[a]|e[a+1]<<8)>>(n&7)&t},al=function(e,n){var t=n/8|0;return(e[t]|e[t+1]<<8|e[t+2]<<16)>>(n&7)},sg=function(e){return(e+7)/8|0},ho=function(e,n,t){return(n==null||n<0)&&(n=0),(t==null||t>e.length)&&(t=e.length),new Je(e.subarray(n,t))},lg=["unexpected EOF","invalid block type","invalid length/literal","invalid distance","stream finished","no stream handler",,"no callback","invalid UTF-8 data","extra field too long","date not in range 1980-2099","filename too long","stream finishing","invalid zip data"],cn=function(e,n,t){var a=new Error(n||lg[e]);if(a.code=e,Error.captureStackTrace&&Error.captureStackTrace(a,cn),!t)throw a;return a},og=function(e,n,t,a){var s=e.length,l=a?a.length:0;if(!s||n.f&&!n.l)return t||new Je(0);var o=!t,r=o||n.i!=2,c=n.i;o&&(t=new Je(s*3));var p=function(pe){var we=t.length;if(pe>we){var k=new Je(Math.max(we*2,pe));k.set(t),t=k}},d=n.f||0,h=n.p||0,m=n.b||0,g=n.l,v=n.d,w=n.m,C=n.n,_=s*8;do{if(!g){d=gn(e,h,1);var T=gn(e,h+1,3);if(h+=3,T)if(T==1)g=tg,v=ag,w=9,C=5;else if(T==2){var x=gn(e,h,31)+257,K=gn(e,h+10,15)+4,F=x+gn(e,h+5,31)+1;h+=14;for(var H=new Je(F),j=new Je(19),Y=0;Y >4;if(y<16)H[Y++]=y;else{var J=0,ne=0;for(y==16?(ne=3+gn(e,h,3),h+=2,J=H[Y-1]):y==17?(ne=3+gn(e,h,7),h+=3):y==18&&(ne=11+gn(e,h,127),h+=7);ne--;)H[Y++]=J}}var W=H.subarray(0,x),Oe=H.subarray(x);w=tl(W),C=tl(Oe),g=ua(W,w,1),v=ua(Oe,C,1)}else cn(1);else{var y=sg(h)+4,S=e[y-4]|e[y-3]<<8,R=y+S;if(R>s){c&&cn(0);break}r&&p(m+S),t.set(e.subarray(y,R),m),n.b=m+=S,n.p=h=R*8,n.f=d;continue}if(h>_){c&&cn(0);break}}r&&p(m+131072);for(var wn=(1< >4;if(h+=J&15,h>_){c&&cn(0);break}if(J||cn(2),en<256)t[m++]=en;else if(en==256){Ne=h,g=null;break}else{var Vn=en-254;if(en>264){var Y=en-257,Cn=Xp[Y];Vn=gn(e,h,(1< >4;De||cn(3),h+=De&15;var Oe=ng[L];if(L>3){var Cn=Qp[L];Oe+=al(e,h)&(1< _){c&&cn(0);break}r&&p(m+131072);var U=m+Vn;if(m >4>7||(e[0]<<8|e[1])%31)&&cn(6,"invalid zlib data"),(e[1]>>5&1)==+!n&&cn(6,"invalid zlib data: "+(e[1]&32?"need":"unexpected")+" dictionary"),(e[1]>>3&4)+2};function pg(e,n){return og(e.subarray(ig(e,n&&n.dictionary),-4),{i:2},n&&n.out,n&&n.dictionary)}var Gr=typeof TextEncoder<"u"&&new TextEncoder,Pl=typeof TextDecoder<"u"&&new TextDecoder,cg=0;try{Pl.decode(rg,{stream:!0}),cg=1}catch{}var ug=function(e){for(var n="",t=0;;){var a=e[t++],s=(a>127)+(a>223)+(a>239);if(t+s>e.length)return{s:n,r:ho(e,t-1)};s?s==3?(a=((a&15)<<18|(e[t++]&63)<<12|(e[t++]&63)<<6|e[t++]&63)-65536,n+=String.fromCharCode(55296|a>>10,56320|a&1023)):s&1?n+=String.fromCharCode((a&31)<<6|e[t++]&63):n+=String.fromCharCode((a&15)<<12|(e[t++]&63)<<6|e[t++]&63):n+=String.fromCharCode(a)}};function dg(e,n){if(n){for(var t=new Je(e.length),a=0;a >1)),o=0,r=function(d){l[o++]=d},a=0;a l.length){var c=new Je(o+8+(s-a<<1));c.set(l),l=c}var p=e.charCodeAt(a);p<128||n?r(p):p<2048?(r(192|p>>6),r(128|p&63)):p>55295&&p<57344?(p=65536+(p&1047552)|e.charCodeAt(++a)&1023,r(240|p>>18),r(128|p>>12&63),r(128|p>>6&63),r(128|p&63)):(r(224|p>>12),r(128|p>>6&63),r(128|p&63))}return ho(l,0,o)}function hg(e,n){if(n){for(var t="",a=0;atypeof e<"u",ac=Object.keys,de=({name:e="",color:n="currentColor"},{slots:t})=>{var a;return i("svg",{xmlns:"http://www.w3.org/2000/svg",class:["icon",`${e}-icon`],viewBox:"0 0 1024 1024",fill:n,"aria-label":`${e} icon`},(a=t.default)==null?void 0:a.call(t))};de.displayName="IconBase";const bt=({size:e=48,stroke:n=4,wrapper:t=!0,height:a=2*e})=>{const s=i("svg",{xmlns:"http://www.w3.org/2000/svg",width:e,height:e,preserveAspectRatio:"xMidYMid",viewBox:"25 25 50 50"},[i("animateTransform",{attributeName:"transform",type:"rotate",dur:"2s",keyTimes:"0;1",repeatCount:"indefinite",values:"0;360"}),i("circle",{cx:"50",cy:"50",r:"20",fill:"none",stroke:"currentColor","stroke-width":n,"stroke-linecap":"round"},[i("animate",{attributeName:"stroke-dasharray",dur:"1.5s",keyTimes:"0;0.5;1",repeatCount:"indefinite",values:"1,200;90,200;1,200"}),i("animate",{attributeName:"stroke-dashoffset",dur:"1.5s",keyTimes:"0;0.5;1",repeatCount:"indefinite",values:"0;-35px;-125px"})])]);return t?i("div",{class:"loading-icon-wrapper",style:`display:flex;align-items:center;justify-content:center;height:${a}px`},s):s};bt.displayName="LoadingIcon";const sc=(e,{slots:n})=>{var t;return(t=n.default)==null?void 0:t.call(n)},mo=()=>i(de,{name:"github"},()=>i("path",{d:"M511.957 21.333C241.024 21.333 21.333 240.981 21.333 512c0 216.832 140.544 400.725 335.574 465.664 24.49 4.395 32.256-10.07 32.256-23.083 0-11.69.256-44.245 0-85.205-136.448 29.61-164.736-64.64-164.736-64.64-22.315-56.704-54.4-71.765-54.4-71.765-44.587-30.464 3.285-29.824 3.285-29.824 49.195 3.413 75.179 50.517 75.179 50.517 43.776 75.008 114.816 53.333 142.762 40.79 4.523-31.66 17.152-53.377 31.19-65.537-108.971-12.458-223.488-54.485-223.488-242.602 0-53.547 19.114-97.323 50.517-131.67-5.035-12.33-21.93-62.293 4.779-129.834 0 0 41.258-13.184 134.912 50.346a469.803 469.803 0 0 1 122.88-16.554c41.642.213 83.626 5.632 122.88 16.554 93.653-63.488 134.784-50.346 134.784-50.346 26.752 67.541 9.898 117.504 4.864 129.834 31.402 34.347 50.474 78.123 50.474 131.67 0 188.586-114.73 230.016-224.042 242.09 17.578 15.232 33.578 44.672 33.578 90.454v135.85c0 13.142 7.936 27.606 32.854 22.87C862.25 912.597 1002.667 728.747 1002.667 512c0-271.019-219.648-490.667-490.71-490.667z"}));mo.displayName="GitHubIcon";const go=()=>i(de,{name:"gitlab"},()=>i("path",{d:"M229.333 78.688C223.52 62 199.895 62 193.895 78.688L87.958 406.438h247.5c-.188 0-106.125-327.75-106.125-327.75zM33.77 571.438c-4.875 15 .563 31.687 13.313 41.25l464.812 345L87.77 406.438zm301.5-165 176.813 551.25 176.812-551.25zm655.125 165-54-165-424.312 551.25 464.812-345c12.938-9.563 18.188-26.25 13.5-41.25zM830.27 78.688c-5.812-16.688-29.437-16.688-35.437 0l-106.125 327.75h247.5z"}));go.displayName="GitLabIcon";const ko=()=>i(de,{name:"gitee"},()=>i("path",{d:"M512 992C246.92 992 32 777.08 32 512S246.92 32 512 32s480 214.92 480 480-214.92 480-480 480zm242.97-533.34H482.39a23.7 23.7 0 0 0-23.7 23.7l-.03 59.28c0 13.08 10.59 23.7 23.7 23.7h165.96a23.7 23.7 0 0 1 23.7 23.7v11.85a71.1 71.1 0 0 1-71.1 71.1H375.71a23.7 23.7 0 0 1-23.7-23.7V423.11a71.1 71.1 0 0 1 71.1-71.1h331.8a23.7 23.7 0 0 0 23.7-23.7l.06-59.25a23.73 23.73 0 0 0-23.7-23.73H423.11a177.78 177.78 0 0 0-177.78 177.75v331.83c0 13.08 10.62 23.7 23.7 23.7h349.62a159.99 159.99 0 0 0 159.99-159.99V482.33a23.7 23.7 0 0 0-23.7-23.7z"}));ko.displayName="GiteeIcon";const fo=()=>i(de,{name:"bitbucket"},()=>i("path",{d:"M575.256 490.862c6.29 47.981-52.005 85.723-92.563 61.147-45.714-20.004-45.714-92.562-1.133-113.152 38.29-23.442 93.696 7.424 93.696 52.005zm63.451-11.996c-10.276-81.152-102.29-134.839-177.152-101.156-47.433 21.138-79.433 71.424-77.129 124.562 2.853 69.705 69.157 126.866 138.862 120.576S647.3 548.571 638.708 478.83zm136.558-309.723c-25.161-33.134-67.986-38.839-105.728-45.13-106.862-17.151-216.576-17.7-323.438 1.134-35.438 5.706-75.447 11.996-97.719 43.996 36.572 34.304 88.576 39.424 135.424 45.129 84.553 10.862 171.447 11.447 256 .585 47.433-5.705 99.987-10.276 135.424-45.714zm32.585 591.433c-16.018 55.99-6.839 131.438-66.304 163.986-102.29 56.576-226.304 62.867-338.87 42.862-59.43-10.862-129.135-29.696-161.72-85.723-14.3-54.858-23.442-110.848-32.585-166.84l3.438-9.142 10.276-5.157c170.277 112.567 408.576 112.567 579.438 0 26.844 8.01 6.84 40.558 6.29 60.014zm103.424-549.157c-19.42 125.148-41.728 249.71-63.415 374.272-6.29 36.572-41.728 57.162-71.424 72.558-106.862 53.724-231.424 62.866-348.562 50.286-79.433-8.558-160.585-29.696-225.134-79.433-30.28-23.443-30.28-63.415-35.986-97.134-20.005-117.138-42.862-234.277-57.161-352.585 6.839-51.42 64.585-73.728 107.447-89.71 57.16-21.138 118.272-30.866 178.87-36.571 129.134-12.58 261.157-8.01 386.304 28.562 44.581 13.13 92.563 31.415 122.844 69.705 13.714 17.7 9.143 40.01 6.29 60.014z"}));fo.displayName="BitbucketIcon";const vo=()=>i(de,{name:"source"},()=>i("path",{d:"M601.92 475.2c0 76.428-8.91 83.754-28.512 99.594-14.652 11.88-43.956 14.058-78.012 16.434-18.81 1.386-40.392 2.97-62.172 6.534-18.612 2.97-36.432 9.306-53.064 17.424V299.772c37.818-21.978 63.36-62.766 63.36-109.692 0-69.894-56.826-126.72-126.72-126.72S190.08 120.186 190.08 190.08c0 46.926 25.542 87.714 63.36 109.692v414.216c-37.818 21.978-63.36 62.766-63.36 109.692 0 69.894 56.826 126.72 126.72 126.72s126.72-56.826 126.72-126.72c0-31.086-11.286-59.598-29.7-81.576 13.266-9.504 27.522-17.226 39.996-19.206 16.038-2.574 32.868-3.762 50.688-5.148 48.312-3.366 103.158-7.326 148.896-44.55 61.182-49.698 74.25-103.158 75.24-187.902V475.2h-126.72zM316.8 126.72c34.848 0 63.36 28.512 63.36 63.36s-28.512 63.36-63.36 63.36-63.36-28.512-63.36-63.36 28.512-63.36 63.36-63.36zm0 760.32c-34.848 0-63.36-28.512-63.36-63.36s28.512-63.36 63.36-63.36 63.36 28.512 63.36 63.36-28.512 63.36-63.36 63.36zM823.68 158.4h-95.04V63.36h-126.72v95.04h-95.04v126.72h95.04v95.04h126.72v-95.04h95.04z"}));vo.displayName="SourceIcon";const Ke=(e,n)=>{var a;const t=(a=(n==null?void 0:n._instance)||_t())==null?void 0:a.appContext.components;return t?e in t||We(e)in t||Kt(We(e))in t:!1},gg=e=>/\b(?:Android|iPhone)/i.test(e),kg=e=>/version\/([\w.]+) .*(mobile ?safari|safari)/i.test(e),fg=e=>[/\((ipad);[-\w),; ]+apple/i,/applecoremedia\/[\w.]+ \((ipad)/i,/\b(ipad)\d\d?,\d\d?[;\]].+ios/i].some(n=>n.test(e)),Vs=(e,n)=>{let t=1;for(let a=0;a >6;return t+=t<<3,t^=t>>11,t%n};let vg=class{constructor(){this.messageElements={};const n="message-container",t=document.getElementById(n);t?this.containerElement=t:(this.containerElement=document.createElement("div"),this.containerElement.id=n,document.body.appendChild(this.containerElement))}pop(n,t=2e3){const a=document.createElement("div"),s=Date.now();return a.className="message move-in",a.innerHTML=n,this.containerElement.appendChild(a),this.messageElements[s]=a,t>0&&setTimeout(()=>{this.close(s)},t),s}close(n){if(n){const t=this.messageElements[n];t.classList.remove("move-in"),t.classList.add("move-out"),t.addEventListener("animationend",()=>{t.remove(),delete this.messageElements[n]})}else ac(this.messageElements).forEach(t=>this.close(Number(t)))}destroy(){document.body.removeChild(this.containerElement)}};const lc=/#.*$/u,_g=e=>{const n=lc.exec(e);return n?n[0]:""},Yr=e=>decodeURI(e).replace(lc,"").replace(/\/index\.html$/iu,"/").replace(/\.html$/iu,"").replace(/(README|index)?\.md$/iu,""),oc=(e,n)=>{if(!mg(n))return!1;const t=Yr(e.path),a=Yr(n),s=_g(n);return s?s===e.hash&&(!a||t===a):t===a};let bg=class{constructor(){this.popupElements={};const n="popup-container",t=document.getElementById(n);t?this.containerElement=t:(this.containerElement=document.createElement("div"),this.containerElement.id=n,document.body.appendChild(this.containerElement))}emit(n,t){const a=document.createElement("div"),s=document.createElement("div"),l=Date.now();return this.containerElement.appendChild(a),this.popupElements[l]=a,a.className="popup-wrapper appear",a.appendChild(s),a.addEventListener("click",()=>this.close(l)),s.className="popup-container",s.innerHTML=n,typeof t=="number"&&setTimeout(()=>{this.close(l)},t),l}close(n){if(n){const t=this.popupElements[n];t.classList.replace("appear","disappear"),t.children[0].addEventListener("animationend",()=>{t.remove(),delete this.popupElements[n]})}else ac(this.popupElements).forEach(t=>this.close(Number(t)))}destroy(){document.body.removeChild(this.containerElement)}};const yg=e=>yn(e)?e:`https://github.com/${e}`,_o=e=>!yn(e)||/github\.com/.test(e)?"GitHub":/bitbucket\.org/.test(e)?"Bitbucket":/gitlab\.com/.test(e)?"GitLab":/gitee\.com/.test(e)?"Gitee":null,wg=()=>{const{availWidth:e,availHeight:n}=screen,{screenLeft:t,screenTop:a,innerWidth:s,innerHeight:l}=window,o=Math.max(e/2,600),r=Math.max(n/2,400);return{width:o,height:r,left:t+s/2-o/2,top:a+l/2-r/2}},Cg=(e,n="_blank",t=["resizable","status"])=>{var r,c;const{width:a,height:s,left:l,top:o}=wg();(c=(r=window.open(e,n,`width=${a},height=${s},left=${l},top=${o},${t.join(",")}`))==null?void 0:r.focus)==null||c.call(r)};var Eg=e=>Object.prototype.toString.call(e)==="[object Object]",wa=e=>typeof e=="string";const rc=Array.isArray,Xr=e=>Eg(e)&&wa(e.name),Ca=(e,n=!1)=>e?rc(e)?e.map(t=>wa(t)?{name:t}:Xr(t)?t:null).filter(t=>t!==null):wa(e)?[{name:e}]:Xr(e)?[e]:(console.error(`Expect "author" to be \`AuthorInfo[] | AuthorInfo | string[] | string ${n?"":"| false"} | undefined\`, but got`,e),[]):[],ic=(e,n)=>{if(e){if(rc(e)&&e.every(wa))return e;if(wa(e))return[e];console.error(`Expect ${n||"value"} to be \`string[] | string | undefined\`, but got`,e)}return[]},pc=e=>ic(e,"category"),cc=e=>ic(e,"tag");function bo(e,n){let t,a,s;const l=q(!0),o=()=>{l.value=!0,s()};oe(e,o,{flush:"sync"});const r=typeof n=="function"?n:n.get,c=typeof n=="function"?void 0:n.set,p=eo((d,h)=>(a=d,s=h,{get(){return l.value&&(t=r(),l.value=!1),a(),t},set(m){c==null||c(m)}}));return Object.isExtensible(p)&&(p.trigger=o),p}function tt(e){return Ii()?(od(e),!0):!1}function Xe(e){return typeof e=="function"?e():fn(e)}const Va=typeof window<"u"&&typeof document<"u";typeof WorkerGlobalScope<"u"&&globalThis instanceof WorkerGlobalScope;const xg=Object.prototype.toString,Tg=e=>xg.call(e)==="[object Object]",Bt=()=>{},Qr=Sg();function Sg(){var e,n;return Va&&((e=window==null?void 0:window.navigator)==null?void 0:e.userAgent)&&(/iP(ad|hone|od)/.test(window.navigator.userAgent)||((n=window==null?void 0:window.navigator)==null?void 0:n.maxTouchPoints)>2&&/iPad|Macintosh/.test(window==null?void 0:window.navigator.userAgent))}function yo(e,n){function t(...a){return new Promise((s,l)=>{Promise.resolve(e(()=>n.apply(this,a),{fn:n,thisArg:this,args:a})).then(s).catch(l)})}return t}const uc=e=>e();function Lg(e,n={}){let t,a,s=Bt;const l=r=>{clearTimeout(r),s(),s=Bt};return r=>{const c=Xe(e),p=Xe(n.maxWait);return t&&l(t),c<=0||p!==void 0&&p<=0?(a&&(l(a),a=null),Promise.resolve(r())):new Promise((d,h)=>{s=n.rejectOnCancel?h:d,p&&!a&&(a=setTimeout(()=>{t&&l(t),a=null,d(r())},p)),t=setTimeout(()=>{a&&l(a),a=null,d(r())},c)})}}function Pg(e,n=!0,t=!0,a=!1){let s=0,l,o=!0,r=Bt,c;const p=()=>{l&&(clearTimeout(l),l=void 0,r(),r=Bt)};return h=>{const m=Xe(e),g=Date.now()-s,v=()=>c=h();return p(),m<=0?(s=Date.now(),v()):(g>m&&(t||!o)?(s=Date.now(),v()):n&&(c=new Promise((w,C)=>{r=a?C:w,l=setTimeout(()=>{s=Date.now(),o=!0,w(v()),p()},Math.max(0,m-g))})),!t&&!l&&(l=setTimeout(()=>o=!0,m)),o=!1,c)}}function Ig(e=uc){const n=q(!0);function t(){n.value=!1}function a(){n.value=!0}const s=(...l)=>{n.value&&e(...l)};return{isActive:nt(n),pause:t,resume:a,eventFilter:s}}function Ag(e){let n;function t(){return n||(n=e()),n}return t.reset=async()=>{const a=n;n=void 0,a&&await a},t}function Vg(e){return e||_t()}function Og(...e){if(e.length!==1)return qt(...e);const n=e[0];return typeof n=="function"?nt(eo(()=>({get:n,set:Bt}))):q(n)}function wo(e,n=200,t={}){return yo(Lg(n,t),e)}function Dg(e,n=200,t=!1,a=!0,s=!1){return yo(Pg(n,t,a,s),e)}function Rg(e,n,t={}){const{eventFilter:a=uc,...s}=t;return oe(e,yo(a,n),s)}function Hg(e,n,t={}){const{eventFilter:a,...s}=t,{eventFilter:l,pause:o,resume:r,isActive:c}=Ig(a);return{stop:Rg(e,n,{...s,eventFilter:l}),pause:o,resume:r,isActive:c}}function Os(e,n=!0,t){Vg()?se(e,t):n?e():vt(e)}function Fg(e,n,t={}){const{immediate:a=!0}=t,s=q(!1);let l=null;function o(){l&&(clearTimeout(l),l=null)}function r(){s.value=!1,o()}function c(...p){o(),s.value=!0,l=setTimeout(()=>{s.value=!1,l=null,e(...p)},Xe(n))}return a&&(s.value=!0,Va&&c()),tt(r),{isPending:nt(s),start:c,stop:r}}function ks(e=!1,n={}){const{truthyValue:t=!0,falsyValue:a=!1}=n,s=$e(e),l=q(e);function o(r){if(arguments.length)return l.value=r,l.value;{const c=Xe(t);return l.value=l.value===c?Xe(a):c,l.value}}return s?o:[l,o]}function Sn(e){var n;const t=Xe(e);return(n=t==null?void 0:t.$el)!=null?n:t}const Nn=Va?window:void 0,dc=Va?window.document:void 0,hc=Va?window.navigator:void 0;function Pe(...e){let n,t,a,s;if(typeof e[0]=="string"||Array.isArray(e[0])?([t,a,s]=e,n=Nn):[n,t,a,s]=e,!n)return Bt;Array.isArray(t)||(t=[t]),Array.isArray(a)||(a=[a]);const l=[],o=()=>{l.forEach(d=>d()),l.length=0},r=(d,h,m,g)=>(d.addEventListener(h,m,g),()=>d.removeEventListener(h,m,g)),c=oe(()=>[Sn(n),Xe(s)],([d,h])=>{if(o(),!d)return;const m=Tg(h)?{...h}:h;l.push(...t.flatMap(g=>a.map(v=>r(d,g,v,m))))},{immediate:!0,flush:"post"}),p=()=>{c(),o()};return tt(p),p}function Ng(){const e=q(!1);return _t()&&se(()=>{e.value=!0}),e}function Gt(e){const n=Ng();return b(()=>(n.value,!!e()))}function mc(e,n={}){const{window:t=Nn}=n,a=Gt(()=>t&&"matchMedia"in t&&typeof t.matchMedia=="function");let s;const l=q(!1),o=p=>{l.value=p.matches},r=()=>{s&&("removeEventListener"in s?s.removeEventListener("change",o):s.removeListener(o))},c=to(()=>{a.value&&(r(),s=t.matchMedia(Xe(e)),"addEventListener"in s?s.addEventListener("change",o):s.addListener(o),l.value=s.matches)});return tt(()=>{c(),r(),s=void 0}),l}function Zr(e,n={}){const{controls:t=!1,navigator:a=hc}=n,s=Gt(()=>a&&"permissions"in a);let l;const o=typeof e=="string"?{name:e}:e,r=q(),c=()=>{l&&(r.value=l.state)},p=Ag(async()=>{if(s.value){if(!l)try{l=await a.permissions.query(o),Pe(l,"change",c),c()}catch{r.value="prompt"}return l}});return p(),t?{state:r,isSupported:s,query:p}:r}function jg(e={}){const{navigator:n=hc,read:t=!1,source:a,copiedDuring:s=1500,legacy:l=!1}=e,o=Gt(()=>n&&"clipboard"in n),r=Zr("clipboard-read"),c=Zr("clipboard-write"),p=b(()=>o.value||l),d=q(""),h=q(!1),m=Fg(()=>h.value=!1,s);function g(){o.value&&r.value!=="denied"?n.clipboard.readText().then(_=>{d.value=_}):d.value=C()}p.value&&t&&Pe(["copy","cut"],g);async function v(_=Xe(a)){p.value&&_!=null&&(o.value&&c.value!=="denied"?await n.clipboard.writeText(_):w(_),d.value=_,h.value=!0,m.start())}function w(_){const T=document.createElement("textarea");T.value=_??"",T.style.position="absolute",T.style.opacity="0",document.body.appendChild(T),T.select(),document.execCommand("copy"),T.remove()}function C(){var _,T,y;return(y=(T=(_=document==null?void 0:document.getSelection)==null?void 0:_.call(document))==null?void 0:T.toString())!=null?y:""}return{isSupported:p,text:d,copied:h,copy:v}}const Ga=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{},Ya="__vueuse_ssr_handlers__",Mg=$g();function $g(){return Ya in Ga||(Ga[Ya]=Ga[Ya]||{}),Ga[Ya]}function Bg(e,n){return Mg[e]||n}function Ug(e){return e==null?"any":e instanceof Set?"set":e instanceof Map?"map":e instanceof Date?"date":typeof e=="boolean"?"boolean":typeof e=="string"?"string":typeof e=="object"?"object":Number.isNaN(e)?"any":"number"}const zg={boolean:{read:e=>e==="true",write:e=>String(e)},object:{read:e=>JSON.parse(e),write:e=>JSON.stringify(e)},number:{read:e=>Number.parseFloat(e),write:e=>String(e)},any:{read:e=>e,write:e=>String(e)},string:{read:e=>e,write:e=>String(e)},map:{read:e=>new Map(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e.entries()))},set:{read:e=>new Set(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e))},date:{read:e=>new Date(e),write:e=>e.toISOString()}},ei="vueuse-storage";function Co(e,n,t,a={}){var s;const{flush:l="pre",deep:o=!0,listenToStorageChanges:r=!0,writeDefaults:c=!0,mergeDefaults:p=!1,shallow:d,window:h=Nn,eventFilter:m,onError:g=H=>{console.error(H)},initOnMounted:v}=a,w=(d?ge:q)(typeof n=="function"?n():n);if(!t)try{t=Bg("getDefaultStorage",()=>{var H;return(H=Nn)==null?void 0:H.localStorage})()}catch(H){g(H)}if(!t)return w;const C=Xe(n),_=Ug(C),T=(s=a.serializer)!=null?s:zg[_],{pause:y,resume:S}=Hg(w,()=>R(w.value),{flush:l,deep:o,eventFilter:m});return h&&r&&Os(()=>{Pe(h,"storage",F),Pe(h,ei,K),v&&F()}),v||F(),w;function R(H){try{if(H==null)t.removeItem(e);else{const j=T.write(H),Y=t.getItem(e);Y!==j&&(t.setItem(e,j),h&&h.dispatchEvent(new CustomEvent(ei,{detail:{key:e,oldValue:Y,newValue:j,storageArea:t}})))}}catch(j){g(j)}}function x(H){const j=H?H.newValue:t.getItem(e);if(j==null)return c&&C!=null&&t.setItem(e,T.write(C)),C;if(!H&&p){const Y=T.read(j);return typeof p=="function"?p(Y,C):_==="object"&&!Array.isArray(Y)?{...C,...Y}:Y}else return typeof j!="string"?j:T.read(j)}function K(H){F(H.detail)}function F(H){if(!(H&&H.storageArea!==t)){if(H&&H.key==null){w.value=C;return}if(!(H&&H.key!==e)){y();try{(H==null?void 0:H.newValue)!==T.write(w.value)&&(w.value=x(H))}catch(j){g(j)}finally{H?vt(S):S()}}}}}function Kg(e){return mc("(prefers-color-scheme: dark)",e)}function gc(e,n,t={}){const{window:a=Nn,...s}=t;let l;const o=Gt(()=>a&&"MutationObserver"in a),r=()=>{l&&(l.disconnect(),l=void 0)},c=oe(()=>Sn(e),h=>{r(),o.value&&a&&h&&(l=new MutationObserver(n),l.observe(h,s))},{immediate:!0}),p=()=>l==null?void 0:l.takeRecords(),d=()=>{r(),c()};return tt(d),{isSupported:o,stop:d,takeRecords:p}}function qg(e,n,t={}){const{window:a=Nn,...s}=t;let l;const o=Gt(()=>a&&"ResizeObserver"in a),r=()=>{l&&(l.disconnect(),l=void 0)},c=b(()=>Array.isArray(e)?e.map(h=>Sn(h)):[Sn(e)]),p=oe(c,h=>{if(r(),o.value&&a){l=new ResizeObserver(n);for(const m of h)m&&l.observe(m,s)}},{immediate:!0,flush:"post",deep:!0}),d=()=>{r(),p()};return tt(d),{isSupported:o,stop:d}}function Jg(e,n={width:0,height:0},t={}){const{window:a=Nn,box:s="content-box"}=t,l=b(()=>{var h,m;return(m=(h=Sn(e))==null?void 0:h.namespaceURI)==null?void 0:m.includes("svg")}),o=q(n.width),r=q(n.height),{stop:c}=qg(e,([h])=>{const m=s==="border-box"?h.borderBoxSize:s==="content-box"?h.contentBoxSize:h.devicePixelContentBoxSize;if(a&&l.value){const g=Sn(e);if(g){const v=a.getComputedStyle(g);o.value=Number.parseFloat(v.width),r.value=Number.parseFloat(v.height)}}else if(m){const g=Array.isArray(m)?m:[m];o.value=g.reduce((v,{inlineSize:w})=>v+w,0),r.value=g.reduce((v,{blockSize:w})=>v+w,0)}else o.value=h.contentRect.width,r.value=h.contentRect.height},t);Os(()=>{const h=Sn(e);h&&(o.value="offsetWidth"in h?h.offsetWidth:n.width,r.value="offsetHeight"in h?h.offsetHeight:n.height)});const p=oe(()=>Sn(e),h=>{o.value=h?n.width:0,r.value=h?n.height:0});function d(){c(),p()}return{width:o,height:r,stop:d}}const ni=["fullscreenchange","webkitfullscreenchange","webkitendfullscreen","mozfullscreenchange","MSFullscreenChange"];function Eo(e,n={}){const{document:t=dc,autoExit:a=!1}=n,s=b(()=>{var _;return(_=Sn(e))!=null?_:t==null?void 0:t.querySelector("html")}),l=q(!1),o=b(()=>["requestFullscreen","webkitRequestFullscreen","webkitEnterFullscreen","webkitEnterFullScreen","webkitRequestFullScreen","mozRequestFullScreen","msRequestFullscreen"].find(_=>t&&_ in t||s.value&&_ in s.value)),r=b(()=>["exitFullscreen","webkitExitFullscreen","webkitExitFullScreen","webkitCancelFullScreen","mozCancelFullScreen","msExitFullscreen"].find(_=>t&&_ in t||s.value&&_ in s.value)),c=b(()=>["fullScreen","webkitIsFullScreen","webkitDisplayingFullscreen","mozFullScreen","msFullscreenElement"].find(_=>t&&_ in t||s.value&&_ in s.value)),p=["fullscreenElement","webkitFullscreenElement","mozFullScreenElement","msFullscreenElement"].find(_=>t&&_ in t),d=Gt(()=>s.value&&t&&o.value!==void 0&&r.value!==void 0&&c.value!==void 0),h=()=>p?(t==null?void 0:t[p])===s.value:!1,m=()=>{if(c.value){if(t&&t[c.value]!=null)return t[c.value];{const _=s.value;if((_==null?void 0:_[c.value])!=null)return!!_[c.value]}}return!1};async function g(){if(!(!d.value||!l.value)){if(r.value)if((t==null?void 0:t[r.value])!=null)await t[r.value]();else{const _=s.value;(_==null?void 0:_[r.value])!=null&&await _[r.value]()}l.value=!1}}async function v(){if(!d.value||l.value)return;m()&&await g();const _=s.value;o.value&&(_==null?void 0:_[o.value])!=null&&(await _[o.value](),l.value=!0)}async function w(){await(l.value?g():v())}const C=()=>{const _=m();(!_||_&&h())&&(l.value=_)};return Pe(t,ni,C,!1),Pe(()=>Sn(s),ni,C,!1),a&&tt(g),{isSupported:d,isFullscreen:l,enter:v,exit:g,toggle:w}}function sl(e){return typeof Window<"u"&&e instanceof Window?e.document.documentElement:typeof Document<"u"&&e instanceof Document?e.documentElement:e}function kc(e){const n=window.getComputedStyle(e);if(n.overflowX==="scroll"||n.overflowY==="scroll"||n.overflowX==="auto"&&e.clientWidth 1?!0:(n.preventDefault&&n.preventDefault(),!1)}const Xa=new WeakMap;function fc(e,n=!1){const t=q(n);let a=null,s;oe(Og(e),r=>{const c=sl(Xe(r));if(c){const p=c;Xa.get(p)||Xa.set(p,s),t.value&&(p.style.overflow="hidden")}},{immediate:!0});const l=()=>{const r=sl(Xe(e));!r||t.value||(Qr&&(a=Pe(r,"touchmove",c=>{Wg(c)},{passive:!1})),r.style.overflow="hidden",t.value=!0)},o=()=>{var r;const c=sl(Xe(e));!c||!t.value||(Qr&&(a==null||a()),c.style.overflow=(r=Xa.get(c))!=null?r:"",Xa.delete(c),t.value=!1)};return tt(o),b({get(){return t.value},set(r){r?l():o()}})}let Gg=0;function Yg(e,n={}){const t=q(!1),{document:a=dc,immediate:s=!0,manual:l=!1,id:o=`vueuse_styletag_${++Gg}`}=n,r=q(e);let c=()=>{};const p=()=>{if(!a)return;const h=a.getElementById(o)||a.createElement("style");h.isConnected||(h.id=o,n.media&&(h.media=n.media),a.head.appendChild(h)),!t.value&&(c=oe(r,m=>{h.textContent=m},{immediate:!0}),t.value=!0)},d=()=>{!a||!t.value||(c(),a.head.removeChild(a.getElementById(o)),t.value=!1)};return s&&!l&&Os(p),l||tt(d),{id:o,css:r,unload:d,load:p,isLoaded:nt(t)}}function Xg(e={}){const{window:n=Nn,behavior:t="auto"}=e;if(!n)return{x:q(0),y:q(0)};const a=q(n.scrollX),s=q(n.scrollY),l=b({get(){return a.value},set(r){scrollTo({left:r,behavior:t})}}),o=b({get(){return s.value},set(r){scrollTo({top:r,behavior:t})}});return Pe(n,"scroll",()=>{a.value=n.scrollX,s.value=n.scrollY},{capture:!1,passive:!0}),{x:l,y:o}}function Qg(e={}){const{window:n=Nn,initialWidth:t=Number.POSITIVE_INFINITY,initialHeight:a=Number.POSITIVE_INFINITY,listenOrientation:s=!0,includeScrollbar:l=!0}=e,o=q(t),r=q(a),c=()=>{n&&(l?(o.value=n.innerWidth,r.value=n.innerHeight):(o.value=n.document.documentElement.clientWidth,r.value=n.document.documentElement.clientHeight))};if(c(),Os(c),Pe("resize",c,{passive:!0}),s){const p=mc("(orientation: portrait)");oe(p,()=>c())}return{width:o,height:r}}const vc=e=>{const n=ln();return b(()=>e[n.value]??{})},Zg=e=>typeof e<"u",ek=Array.isArray,_c=(e,n)=>me(e)&&e.startsWith(n),nk=(e,n)=>me(e)&&e.endsWith(n),tk=Object.entries,ak=Object.keys,sk=e=>_c(e,"/");var lk=V({name:"FontIcon",props:{icon:{type:String,default:""},color:{type:String,default:""},size:{type:[String,Number],default:""}},setup(e){const n=b(()=>{const a=["font-icon icon"],s=`iconfont icon-${e.icon}`;return a.push(s),a}),t=b(()=>{const a={};return e.color&&(a.color=e.color),e.size&&(a["font-size"]=Number.isNaN(Number(e.size))?e.size:`${e.size}px`),ak(a).length?a:null});return()=>e.icon?i("span",{key:e.icon,class:n.value,style:t.value}):null}});const ti="https://codepen.io",ok=e=>{let n="";for(const t in e)t!=="prefill"&&t!=="open"&&(n!==""&&(n+="&"),n+=t+"="+encodeURIComponent(e[t]));return n},bc=e=>{const n=e.preview==="true"?"embed/preview":"embed";if("prefill"in e)return[ti,n,"prefill"].join("/");let t=e["slug-hash"];if(!t)throw new Error("slug-hash is required");return e.token&&(t+="/"+e.token),[ti,e.user||"anon",n,t+"?"+ok(e)].join("/").replace(/\/\//g,"//")},Il=(e,n)=>{const t=document.createElement(e);for(const a in n)Object.prototype.hasOwnProperty.call(n,a)&&t.setAttribute(a,n[a].toString());return t},rk=e=>{const n=Il("form",{class:"code-pen-embed-form",style:"display: none;",method:"post",action:bc(e),target:e.name||""});for(const t in e)t!=="prefill"&&n.append(Il("input",{type:"hidden",name:t,value:String(e[t])}));return n},ik=e=>{const{height:n=300,class:t="",name:a="CodePen Embed"}=e,s={class:`cp_embed_iframe ${t}`,src:bc(e),allowfullscreen:"",allowpaymentrequest:"",allowTransparency:"",frameborder:0,width:"100%",height:n,name:a,scrolling:"no",style:"width: 100%; overflow: hidden; display: block;",title:e["pen-title"]||a};return"prefill"in e||(s.loading="lazy"),e["slug-hash"]&&(s.id=`code-pen-embed-${e["slug-hash"].replace("/","_")}`),Il("iframe",s)},pk=(e,n)=>{if(e.parentNode){const t=document.createElement("div");return t.className="code-pen-embed-wrapper",t.append(n),e.parentNode.replaceChild(t,e),t}return e.append(n),e};let ck=1;const ai=(e,n)=>{const t=typeof n=="string"?document.querySelector(n):n instanceof HTMLElement?n:null;e.user||(e.user="anon"),e.name||(e.name=t?`code-pen-api-${ck++}`:"_blank");const a=document.createDocumentFragment();let s=null;"prefill"in e&&(e.data=JSON.stringify(e.prefill||"{}"),s=rk(e),a.append(s)),t?(a.append(ik(e)),pk(t,a)):document.body.appendChild(a),s&&s.submit()};var uk=V({name:"CodePen",props:{link:{type:String,default:""},user:{type:String,default:""},slugHash:{type:String,default:""},title:{type:String,default:""},height:{type:[String,Number],default:380},theme:{type:String,default:"default"},defaultTab:{type:Array,default:()=>["result"]},status:{type:String,default:"preview"}},setup(e){const n=()=>{const l=/(?:^(?:https?:)?\/\/codepen.io\/|^\/|^)(.*?)\/(?:pen|embed)\/(.*?)\/?$/.exec(e.link);return{user:l==null?void 0:l[1],slugHash:l==null?void 0:l[2]}},t=b(()=>n().user||e.user),a=b(()=>n().slugHash||e.slugHash),s=b(()=>({user:t.value,"slug-hash":a.value,"theme-id":e.theme,"default-tab":e.defaultTab.join(","),"pen-title":e.title,height:e.height,preview:e.status==="preview"?"true":""}));return se(()=>{e.status!=="clicktorun"&&ai(s.value,`.codepen-${a.value}`)}),()=>i("div",{class:["codepen-wrapper",`codepen-${a.value}`]},[e.status==="clicktorun"?i("button",{type:"button",class:"codepen-button",onClick:()=>{ai(s.value,`.codepen-${a.value}`)}},"Run Code"):null,i("span",["See the Pen ",i("a",{href:e.link},[e.title])," by ",i("a",{href:`https://codepen.io/${t.value}`},[t.value])," on ",i("a",{href:"https://codepen.io"},["CodePen"]),"."])])}});const si=e=>me(e)?e:`${e}px`,dk=(e,n=0)=>{const t=ge(),a=b(()=>si(fn(e.width)||"100%")),s=q("auto"),l=c=>{if(me(c)){const[p,d]=c.split(":"),h=Number(p)/Number(d);if(!Number.isNaN(h))return h}return typeof c=="number"?c:16/9},o=c=>{const p=fn(e.height),d=l(fn(e.ratio));return p?si(p):`${Number(c)/d+fn(n)}px`},r=()=>{t.value&&(s.value=o(t.value.clientWidth))};return se(()=>{r(),$e(n)&&oe(n,r),Pe("orientationchange",r),Pe("resize",r)}),{el:t,width:a,height:s,resize:r}},hk=e=>yn(e)?e:Ce(e);var mk={"/":{hint:" 이 브라우저는 PDF를 포함할 수 없습니다. PDF를 보려면 다운로드하십시오: PDF 다운로드
"}};const ll=e=>{console.error(`[PDF]: ${e}`)},gk=e=>{for(;e.firstChild;)e.removeChild(e.firstChild)},kk=e=>e==="string"?document.querySelector(e):e instanceof HTMLElement?e:document.body,fk=e=>{let n="";return e&&(n+=tk(e).map(([t,a])=>t==="noToolbar"?`toolbar=${a?0:1}`:`${encodeURIComponent(t)}=${encodeURIComponent(a)}`).join("&"),n&&(n=`#${n.slice(0,n.length-1)}`)),n},vk=(e,n,t,a,s)=>{gk(n);const l=`${e==="pdfjs"?`${io(Ce(null))}web/viewer.html?file=${encodeURIComponent(t)}`:t}${fk(a)}`,o=e==="pdfjs"||e==="iframe"?"iframe":"embed",r=document.createElement(o);return r.className="pdf-viewer",r.type="application/pdf",r.title=s,r.src=l,r instanceof HTMLIFrameElement&&(r.allow="fullscreen"),n.classList.add("pdf-viewer-container"),n.appendChild(r),n.getElementsByTagName(o)[0]},_k=(e,n,{title:t,hint:a,options:s={}})=>{var v,w;if(typeof window>"u"||!((v=window==null?void 0:window.navigator)!=null&&v.userAgent))return null;const{navigator:l}=window,{userAgent:o}=l,r=Zg(window.Promise),c=fg(o)||gg(o),p=!c&&kg(o),d=!c&&/firefox/iu.test(o)&&o.split("rv:").length>1?parseInt(o.split("rv:")[1].split(".")[0],10)>18:!1,h=!c&&(r||d);if(!me(e))return ll("URL is not valid"),null;const m=kk(n);if(!m)return ll("Target element cannot be determined"),null;const g=t||((w=/\/([^/]+).pdf/.exec(e))==null?void 0:w[1])||"PDF Viewer";return h||!c?vk(p?"iframe":"embed",m,e,s,g):(m.innerHTML=a.replace(/\[url\]/g,e),ll("This browser does not support embedded PDFs"),null)};var bk=V({name:"PDF",props:{url:{type:String,required:!0},title:{type:String,default:""},width:{type:[String,Number],default:"100%"},height:{type:[String,Number],default:void 0},ratio:{type:[String,Number],default:16/9},page:{type:[String,Number],default:1},noToolbar:Boolean,zoom:{type:[String,Number],default:100}},setup(e){const{el:n,width:t,height:a,resize:s}=dk(e),l=vc(mk);return se(()=>{_k(hk(e.url),n.value,{title:e.title,hint:l.value.hint,options:{page:e.page,noToolbar:e.noToolbar,zoom:e.zoom}}),s()}),()=>i("div",{class:"pdf-viewer-wrapper",ref:n,style:{width:t.value,height:a.value}})}}),yk=[{name:"twitter",link:"https://twitter.com/intent/tweet?text=[title]&url=[url]&hashtags=[tags][title]",color:"#000",shape:''},{name:"facebook",link:"https://www.facebook.com/sharer/sharer.php?u=[url]&title=[title]&description=[description]"e=[summary]&hashtag=[tags]",color:"#3c599b",shape:''},{name:"reddit",link:"https://www.reddit.com/submit?title=[title]&url=[url]",color:"#ff4501",shape:''},{name:"telegram",link:"https://t.me/share/url?url=[url]&text=[title]%0D%0A[description|summary]",color:"#158cc7",shape:''},{name:"whatsapp",link:"https://api.whatsapp.com/send?text=[title]%0D%0A[url]%0D%0A[description|summary]",color:"#31B84C",shape:''},{name:"email",link:"mailto:?subject=[title]&body=[url]%0D%0A%0D%0A[description|summary]",color:"#1384FF",action:"open",shape:''}];const Qa=e=>{var n;return((n=document.querySelector(`meta[name="${e}"]`))==null?void 0:n.getAttribute("content"))??null},li=(e,n="")=>{const t=["vp-share-icon",n];return yn(e)||sk(e)?i("img",{class:t,src:e,loading:"lazy","no-view":""}):_c(e,"<")&&nk(e,">")?i("div",{class:t,innerHTML:e}):i("div",{class:[...t,e]})};var wk=V({name:"ShareService",props:{config:{type:Object,default:()=>({})},plain:Boolean,title:{type:String,required:!1},description:{type:String,required:!1},url:{type:String,required:!1},summary:{type:String,required:!1},cover:{type:String,required:!1},tag:{type:[Array,String],required:!1}},setup(e){let n;const t=ke(),a=ye(),s=q(!1),l=()=>{var v;const r=e.title??t.value.title,c=e.description??a.value.description??Qa("description")??Qa("og:description")??Qa("twitter:description"),p=e.url??typeof window>"u"?null:window.location.href,d=e.cover??Qa("og:image"),h=(v=document.querySelector(".theme-default-content :not(a) > img"))==null?void 0:v.getAttribute("src"),m=e.tag??a.value.tag??a.value.tags,g=ek(m)?m.filter(me).join(","):me(m)?m:null;return e.config.link.replace(/\[([^\]]+)\]/g,(w,C)=>{const _=C.split("|");for(const T of _){if(T==="url"&&p)return p;if(T==="title"&&r)return r;if(T==="description"&&c)return c;if(T==="summary"&&e.summary)return e.summary;if(T==="cover"&&d)return d;if(T==="image"&&h)return h;if(T==="tags"&&g)return g}return""})},o=()=>{const r=l();switch(e.config.action){case"navigate":window.open(r);break;case"open":window.open(r,"_blank");break;case"qrcode":u(()=>import("./browser-D6eOinvE.js").then(c=>c.b),__vite__mapDeps([])).then(({toDataURL:c})=>c(r,{errorCorrectionLevel:"H",width:250,scale:1,margin:1.5})).then(c=>{n.emit(``)});break;default:Cg(r,"share")}};return se(()=>{n=new bg}),()=>{const{config:{name:r,icon:c,shape:p,color:d},plain:h}=e;return[i("button",{type:"button",class:["vp-share-button",{plain:h}],"aria-label":r,"data-balloon-pos":"up",onClick:()=>o()},h?li(p,"plain"):c?li(c):i("div",{class:"vp-share-icon colorful",style:{background:d},innerHTML:p})),s.value?i("div",{class:"share-popup"}):null]}}});const oi=yk;var Ck=V({name:"Share",props:{services:{type:[String,Array],default:()=>oi.map(({name:e})=>e)},titleGetter:{type:Function,default:e=>e.title},descriptionGetter:{type:Function,default:e=>e.frontmatter.description},summaryGetter:{type:Function,default:e=>e.summary},coverGetter:{type:Function,default:e=>e.cover},tagGetter:{type:Function,default:({frontmatter:e})=>e.tag||e.tags},inline:Boolean,colorful:Boolean},setup(e){const n=ke(),t=b(()=>(me(e.services)?e.services.split(","):e.services).map(s=>_n(s)?s.name&&s.link?s:null:oi.find(({name:l})=>l===s)).filter(s=>!!s)),a=b(()=>{const s={};return["titleGetter","descriptionGetter","summaryGetter","coverGetter","tagGetter"].forEach(l=>{if(Rp(e[l])){const o=e[l](n.value);o&&(s[l.replace("Getter","")]=o)}}),s});return()=>i("div",{class:"vp-share-buttons",style:e.inline?{display:"inline-block"}:{}},t.value.map(s=>i(wk,{config:s,...a.value,plain:!e.colorful})))}}),Ek={"/":{source:"소스 코드"}},xk=V({name:"SiteInfo",components:{BitbucketIcon:fo,GiteeIcon:ko,GitHubIcon:mo,GitLabIcon:go,SourceIcon:vo},props:{name:{type:String,required:!0},desc:{type:String,default:""},logo:{type:String,default:""},url:{type:String,required:!0},preview:{type:String,required:!0},repo:{type:String,default:""}},setup(e){const n=vc(Ek),t=b(()=>e.repo?_o(e.repo):null);return()=>i("div",{class:"vp-site-info","data-name":e.name},[i("a",{class:"vp-site-info-navigator",title:e.name,href:e.url,target:"_blank"}),i("div",{class:"vp-site-info-preview",style:{background:`url(${Ce(e.preview)}) center/cover no-repeat`}}),i("div",{class:"vp-site-info-detail"},[e.logo?i("img",{class:"vp-site-info-logo",src:e.logo,alt:"",loading:"lazy","no-view":""}):null,i("div",{class:"vp-site-info-name"},e.name),i("div",{class:"vp-site-info-desc"},e.desc)]),e.repo?i("div",{class:"vp-site-info-source-wrapper"},i("a",{class:"vp-site-info-source",href:e.repo,"aria-label":n.value.source,"data-balloon-pos":"left",title:n.value.source,target:"_blank"},i(an(`${t.value}Icon`)))):null])}}),Tk=V({name:"VidStack",props:{sources:{type:Array,default:()=>[]},tracks:{type:Array,default:()=>[]}},setup(e,{attrs:n}){return se(async()=>{await Promise.all([u(()=>import("./vidstack-player-X42x4ssq.js"),__vite__mapDeps([523,524])),u(()=>import("./vidstack-player-layouts-c3Bu6zPS.js"),__vite__mapDeps([525,524,526])),u(()=>import("./vidstack-player-ui-BQjgjIeO.js"),__vite__mapDeps([527,524,526]))])}),()=>i("media-player",n,[i("media-provider",[n.poster?i("media-poster",{class:"vds-poster",alt:n.alt||n.title}):null,e.sources.map(t=>_n(t)?i("source",t):i("source",{src:t})),e.tracks.map(t=>i("track",t))]),i("media-audio-layout"),i("media-video-layout",n)])}});const yc=({type:e="info",text:n="",vertical:t,color:a},{slots:s})=>{var l;return i("span",{class:["vp-badge",e,{diy:a}],style:{verticalAlign:t??!1,backgroundColor:a??!1}},((l=s.default)==null?void 0:l.call(s))||n)};yc.displayName="Badge";const Sk=Ge({enhance:({app:e})=>{Ke("FontIcon")||e.component("FontIcon",lk),Ke("CodePen")||e.component("CodePen",uk),Ke("PDF")||e.component("PDF",bk),Ke("Share")||e.component("Share",Ck),Ke("SiteInfo")||e.component("SiteInfo",xk),Ke("VidStack")||e.component("VidStack",Tk),Ke("Badge")||e.component("Badge",yc)},setup:()=>{Yg(` @import url("https://at.alicdn.com/t/c/font_2410206_5vb9zlyghj.css"); + `)},rootComponents:[]}),ri=async(e,n)=>{const{path:t,query:a}=e.currentRoute.value,{scrollBehavior:s}=e.options;e.options.scrollBehavior=void 0,await e.replace({path:t,query:a,hash:n}),e.options.scrollBehavior=s},Lk=({headerLinkSelector:e,headerAnchorSelector:n,delay:t,offset:a=5})=>{const s=Pn();Pe("scroll",wo(()=>{var v,w;const o=Math.max(window.scrollY,document.documentElement.scrollTop,document.body.scrollTop);if(Math.abs(o-0)h.some(_=>_.hash===C.hash));for(let C=0;C=(((v=_.parentElement)==null?void 0:v.offsetTop)??0)-a,S=!T||o<(((w=T.parentElement)==null?void 0:w.offsetTop)??0)-a;if(!(y&&S))continue;const x=decodeURIComponent(s.currentRoute.value.hash),K=decodeURIComponent(_.hash);if(x===K)return;if(d){for(let F=C+1;F {const n=ln();return b(()=>e[n.value]??{})},Dk=(e,n)=>{var a;const t=(a=(n==null?void 0:n._instance)||_t())==null?void 0:a.appContext.components;return t?e in t||We(e)in t||Kt(We(e))in t:!1},ol=e=>typeof e=="number",ii=(e,n)=>me(e)&&e.startsWith(n),Rk=(e,n)=>me(e)&&e.endsWith(n),Hk=Object.entries,Fk=Object.keys;let wc=e=>me(e.title)?{title:e.title}:null;const Cc=Symbol(""),Nk=e=>{wc=e},jk=()=>be(Cc),Mk=e=>{e.provide(Cc,wc)};var $k={"/":{title:"목차",empty:"목차 없음"}};const Bk=V({name:"Catalog",props:{base:{type:String,default:""},level:{type:Number,default:3},index:Boolean,hideHeading:Boolean},setup(e){const n=jk(),t=Ds($k),a=ke(),s=K0(),l=Jp(),r=ge(Hk(s.value).map(([p,{meta:d}])=>{const h=n(d);if(!h)return null;const m=p.split("/").length;return{level:Rk(p,"/")?m-2:m-1,base:p.replace(/\/[^/]+\/?$/,"/"),path:p,...h}}).filter(p=>_n(p)&&me(p.title))),c=b(()=>{const p=e.base?Am(io(e.base)):a.value.path.replace(/\/[^/]+$/,"/"),d=p.split("/").length-2,h=[];return r.value.filter(({level:m,path:g})=>{if(!ii(g,p)||g===p)return!1;if(p==="/"){const v=Fk(l.value.locales).filter(w=>w!=="/");if(g==="/404.html"||v.some(w=>ii(g,w)))return!1}return m-d<=e.level}).sort(({title:m,level:g,order:v},{title:w,level:C,order:_})=>{const T=g-C;return T||(ol(v)?ol(_)?v>0?_>0?v-_:-1:_<0?v-_:1:v:ol(_)?_:m.localeCompare(w))}).forEach(m=>{var w;const{base:g,level:v}=m;switch(v-d){case 1:{h.push(m);break}case 2:{const C=h.find(_=>_.path===g);C&&(C.children??(C.children=[])).push(m);break}default:{const C=h.find(_=>_.path===g.replace(/\/[^/]+\/$/,"/"));if(C){const _=(w=C.children)==null?void 0:w.find(T=>T.path===g);_&&(_.children??(_.children=[])).push(m)}}}}),h});return()=>{const p=c.value.some(d=>d.children);return i("div",{class:["vp-catalog-wrapper",{index:e.index}]},[e.hideHeading?null:i("h2",{class:"vp-catalog-main-title"},t.value.title),c.value.length?i(e.index?"ol":"ul",{class:["vp-catalogs",{deep:p}]},c.value.map(({children:d=[],title:h,path:m,content:g})=>{const v=i(Ve,{class:"vp-catalog-title",to:m},()=>g?i(g):h);return i("li",{class:"vp-catalog"},p?[i("h3",{id:h,class:["vp-catalog-child-title",{"has-children":d.length}]},[i("a",{href:`#${h}`,class:"vp-catalog-header-anchor","aria-hidden":!0},"#"),v]),d.length?i(e.index?"ol":"ul",{class:"vp-child-catalogs"},d.map(({children:w=[],content:C,path:_,title:T})=>i("li",{class:"vp-child-catalog"},[i("div",{class:["vp-catalog-sub-title",{"has-children":w.length}]},[i("a",{href:`#${T}`,class:"vp-catalog-header-anchor"},"#"),i(Ve,{class:"vp-catalog-title",to:_},()=>C?i(C):T)]),w.length?i(e.index?"ol":"div",{class:e.index?"vp-sub-catalogs":"vp-sub-catalogs-wrapper"},w.map(({content:y,path:S,title:R})=>e.index?i("li",{class:"vp-sub-catalog"},i(Ve,{to:S},()=>y?i(y):R)):i(Ve,{class:"vp-sub-catalog-link",to:S},()=>y?i(y):R))):null]))):null]:i("div",{class:"vp-catalog-child-title"},v))})):i("p",{class:"vp-empty-catalog"},t.value.empty)])}}}),Uk=Ge({enhance:({app:e})=>{Mk(e),Dk("Catalog",e)||e.component("Catalog",Bk)}}),Ec=e=>{const n=ln();return b(()=>e[n.value]??{})},Al=Array.isArray,xo=(e,n)=>me(e)&&e.startsWith(n),Yt=Object.entries,zk=Object.fromEntries,mt=Object.keys,To=e=>{if(e){if(typeof e=="number")return new Date(e);const n=Date.parse(e.toString());if(!Number.isNaN(n))return new Date(n)}return null},Rs=e=>xo(e,"/");var Kk={"/":{backToTop:"맨 위로"}};const qk=V({name:"BackToTop",setup(e){const n=ye(),t=Ec(Kk),a=ge(),{height:s}=Jg(a),{height:l}=Qg(),{y:o}=Xg(),r=b(()=>n.value.backToTop!==!1&&o.value>100),c=b(()=>o.value/(s.value-l.value)*100);return se(()=>{a.value=document.body}),()=>i(Fn,{name:"back-to-top"},()=>r.value?i("button",{type:"button",class:"vp-back-to-top-button","aria-label":t.value.backToTop,onClick:()=>{window.scrollTo({top:0,behavior:"smooth"})}},[i("span",{class:"vp-scroll-progress",role:"progressbar","aria-labelledby":"loadinglabel","aria-valuenow":c.value},i("svg",i("circle",{cx:"50%",cy:"50%",style:{"stroke-dasharray":`calc(${Math.PI*c.value}% - ${4*Math.PI}px) calc(${Math.PI*100}% - ${4*Math.PI}px)`}}))),i("div",{class:"back-to-top-icon"})]):null)}}),Jk=Ge({rootComponents:[qk]}),Wk=i("svg",{class:"external-link-icon",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",x:"0px",y:"0px",viewBox:"0 0 100 100",width:"15",height:"15"},[i("path",{fill:"currentColor",d:"M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"}),i("polygon",{fill:"currentColor",points:"45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"})]),xc=V({name:"ExternalLinkIcon",props:{locales:{type:Object,required:!1,default:()=>({})}},setup(e){const n=ln(),t=b(()=>e.locales[n.value]??{openInNewWindow:"open in new window"});return()=>i("span",[Wk,i("span",{class:"external-link-icon-sr-only"},t.value.openInNewWindow)])}});var Gk={};const Yk=Gk,Xk=Ge({enhance({app:e}){e.component("ExternalLinkIcon",i(xc,{locales:Yk}))}});/** + * NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress + * @license MIT + */const ce={settings:{minimum:.08,easing:"ease",speed:200,trickle:!0,trickleRate:.02,trickleSpeed:800,barSelector:'[role="bar"]',parent:"body",template:''},status:null,set:e=>{const n=ce.isStarted();e=rl(e,ce.settings.minimum,1),ce.status=e===1?null:e;const t=ce.render(!n),a=t.querySelector(ce.settings.barSelector),s=ce.settings.speed,l=ce.settings.easing;return t.offsetWidth,Qk(o=>{Za(a,{transform:"translate3d("+pi(e)+"%,0,0)",transition:"all "+s+"ms "+l}),e===1?(Za(t,{transition:"none",opacity:"1"}),t.offsetWidth,setTimeout(function(){Za(t,{transition:"all "+s+"ms linear",opacity:"0"}),setTimeout(function(){ce.remove(),o()},s)},s)):setTimeout(()=>o(),s)}),ce},isStarted:()=>typeof ce.status=="number",start:()=>{ce.status||ce.set(0);const e=()=>{setTimeout(()=>{ce.status&&(ce.trickle(),e())},ce.settings.trickleSpeed)};return ce.settings.trickle&&e(),ce},done:e=>!e&&!ce.status?ce:ce.inc(.3+.5*Math.random()).set(1),inc:e=>{let n=ce.status;return n?(typeof e!="number"&&(e=(1-n)*rl(Math.random()*n,.1,.95)),n=rl(n+e,0,.994),ce.set(n)):ce.start()},trickle:()=>ce.inc(Math.random()*ce.settings.trickleRate),render:e=>{if(ce.isRendered())return document.getElementById("nprogress");ci(document.documentElement,"nprogress-busy");const n=document.createElement("div");n.id="nprogress",n.innerHTML=ce.settings.template;const t=n.querySelector(ce.settings.barSelector),a=e?"-100":pi(ce.status||0),s=document.querySelector(ce.settings.parent);return Za(t,{transition:"all 0 linear",transform:"translate3d("+a+"%,0,0)"}),s!==document.body&&ci(s,"nprogress-custom-parent"),s==null||s.appendChild(n),n},remove:()=>{ui(document.documentElement,"nprogress-busy"),ui(document.querySelector(ce.settings.parent),"nprogress-custom-parent");const e=document.getElementById("nprogress");e&&Zk(e)},isRendered:()=>!!document.getElementById("nprogress")},rl=(e,n,t)=>e t?t:e,pi=e=>(-1+e)*100,Qk=function(){const e=[];function n(){const t=e.shift();t&&t(n)}return function(t){e.push(t),e.length===1&&n()}}(),Za=function(){const e=["Webkit","O","Moz","ms"],n={};function t(o){return o.replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,function(r,c){return c.toUpperCase()})}function a(o){const r=document.body.style;if(o in r)return o;let c=e.length;const p=o.charAt(0).toUpperCase()+o.slice(1);let d;for(;c--;)if(d=e[c]+p,d in r)return d;return o}function s(o){return o=t(o),n[o]??(n[o]=a(o))}function l(o,r,c){r=s(r),o.style[r]=c}return function(o,r){for(const c in r){const p=r[c];p!==void 0&&Object.prototype.hasOwnProperty.call(r,c)&&l(o,c,p)}}}(),Tc=(e,n)=>(typeof e=="string"?e:So(e)).indexOf(" "+n+" ")>=0,ci=(e,n)=>{const t=So(e),a=t+n;Tc(t,n)||(e.className=a.substring(1))},ui=(e,n)=>{const t=So(e);if(!Tc(e,n))return;const a=t.replace(" "+n+" "," ");e.className=a.substring(1,a.length-1)},So=e=>(" "+(e.className||"")+" ").replace(/\s+/gi," "),Zk=e=>{e&&e.parentNode&&e.parentNode.removeChild(e)},ef=()=>{se(()=>{const e=Pn(),n=new Set;n.add(e.currentRoute.value.path),e.beforeEach(t=>{n.has(t.path)||ce.start()}),e.afterEach(t=>{n.add(t.path),ce.done()})})},nf=Ge({setup(){ef()}}),tf=JSON.parse('{"encrypt":{},"logo":"/logo.png","repo":"docmoa/docs","docsDir":"docs","print":true,"fullscreen":true,"contributors":true,"lastUpdated":true,"locales":{"/":{"lang":"ko-KR","navbarLocales":{"langName":"한국어","selectLangAriaLabel":"언어 선택"},"metaLocales":{"author":"작성자","date":"작성일","origin":"원본","views":"조회수","category":"카테고리","tag":"태그","readingTime":"읽는 시간","words":"단어","toc":"이 페이지에서","prev":"이전","next":"다음","lastUpdated":"마지막 수정","contributors":"기여자","editLink":"Edit this page on GitHub","print":"인쇄"},"blogLocales":{"article":"게시글","articleList":"글 목록","category":"카테고리","tag":"태그","timeline":"타임라인","timelineTitle":"어제 한 번 더!","all":"모두","intro":"프로필","star":"스타","empty":"$text가 비어있습니다."},"paginationLocales":{"prev":"이전","next":"다음","navigate":"이동","action":"가기","errorText":"1에서 $page 사이의 숫자를 입력하세요!"},"outlookLocales":{"themeColor":"테마 색상","darkmode":"테마 모드","fullscreen":"전체 화면"},"routeLocales":{"skipToContent":"본문으로 건너뛰기","notFoundTitle":"페이지를 찾을 수 없습니다.","notFoundMsg":["여기에는 아무것도 없습니다.","어떻게 여기까지 오셨나요?","4-0-4 입니다.","깨진 링크가 있는 것 같습니다."],"back":"뒤로가기","home":"메인으로","openInNewWindow":"새 창에서 열기"},"navbar":["/",{"text":"How To","icon":"launch","link":"/00-Howto/"},{"text":"Infra","icon":"computer","children":[{"text":"Infrastructure","link":"/01-Infrastructure/"},{"text":"Private-Platform","link":"/02-PrivatePlatform/"},{"text":"Public-Cloud","link":"/03-PublicCloud/"}]},{"text":"Software","icon":"code","link":"/05-Software/"},{"text":"HashiCorp","icon":"workingDirectory","link":"/04-HashiCorp/"},{"text":"Etc.","icon":"flex","link":"/06-etc/"},{"text":"MORE","icon":"more","children":[{"text":"About","icon":"info","link":"/99-about/01-About.html"},{"text":"Thank you","icon":"like","link":"/99-about/02-Thanks.html"}]},{"text":"Tags","icon":"tag","link":"/tag"}],"sidebar":{"/00-Howto/":"structure","/01-Infrastructure/":"structure","/02-PrivatePlatform/":"structure","/03-PublicCloud/":"structure","/04-HashiCorp/":"structure","/05-Software/":"structure","/06-etc/":"structure","/99-about/":"structure","/":["","tag","99-about/01-About"]},"footer":"CC BY-NC-ND 4.0 Licensed | ⓒ 2021-present docmoa™ contributers all rights reserved.","displayFooter":true}}}'),af=q(tf),Sc=()=>af,Lc=Symbol(""),sf=()=>{const e=be(Lc);if(!e)throw new Error("useThemeLocaleData() is called without provider.");return e},lf=(e,n)=>{const{locales:t,...a}=e;return{...a,...t==null?void 0:t[n]}},of=Ge({enhance({app:e}){const n=Sc(),t=e._context.provides[uo],a=b(()=>lf(n.value,t.routeLocale.value));e.provide(Lc,a),Object.defineProperties(e.config.globalProperties,{$theme:{get(){return n.value}},$themeLocale:{get(){return a.value}}})}}),rf=/\b(?:Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini)/i,pf=()=>typeof window<"u"&&window.navigator&&"userAgent"in window.navigator&&rf.test(navigator.userAgent),il=new Map,cf=({delay:e=500,duration:n=2e3,locales:t,selector:a,showInMobile:s})=>{const{copy:l}=jg({legacy:!0}),o=Ec(t),r=ke(),c=h=>{if(!h.hasAttribute("copy-code-registered")){const m=document.createElement("button");m.type="button",m.classList.add("vp-copy-code-button"),m.innerHTML='',m.setAttribute("aria-label",o.value.copy),m.setAttribute("data-copied",o.value.copied),h.parentElement&&h.parentElement.insertBefore(m,h),h.setAttribute("copy-code-registered","")}},p=()=>{vt().then(()=>setTimeout(()=>{a.forEach(h=>{document.querySelectorAll(h).forEach(c)})},e))},d=(h,m,g)=>{let{innerText:v=""}=m;/language-(shellscript|shell|bash|sh|zsh)/.test(h.classList.toString())&&(v=v.replace(/^ *(\$|>) /gm,"")),l(v).then(()=>{g.classList.add("copied"),clearTimeout(il.get(g));const w=setTimeout(()=>{g.classList.remove("copied"),g.blur(),il.delete(g)},n);il.set(g,w)})};se(()=>{const h=!pf()||s;h&&p(),Pe("click",m=>{const g=m.target;if(g.matches('div[class*="language-"] > button.copy')){const v=g.parentElement,w=g.nextElementSibling;w&&d(v,w,g)}else if(g.matches('div[class*="language-"] div.vp-copy-icon')){const v=g.parentElement,w=v.parentElement,C=v.nextElementSibling;C&&d(w,C,v)}}),oe(()=>r.value.path,()=>{h&&p()})})};var uf={"/":{copy:"코드 복사",copied:"복사됨"}},df=['.theme-hope-content div[class*="language-"] pre'];const hf=500,mf=2e3,gf=uf,kf=df,ff=!1,vf=Ge({setup:()=>{cf({selector:kf,locales:gf,duration:mf,delay:hf,showInMobile:ff})}});var _f={"/":{author:"저작권자 :author",license:":license 프로토콜에 따라",link:":link"}},bf={canonical:"https://docmoa.github.io",author:"",license:"CC BY-NC-ND 4.0 Licensed | ⓒ 2021-present docmoa™ contributers all rights reserved.",global:!1,disableCopy:!1,disableSelection:!1,triggerLength:100,maxLength:0};const Kn=bf,{canonical:es}=Kn,yf=()=>{const e=ye(),n=Ds(_f),t=ke(),a=b(()=>!!e.value.copy||e.value.copy!==!1&&Kn.global),s=b(()=>_n(e.value.copy)?e.value.copy:null),l=b(()=>{var g;return((g=s.value)==null?void 0:g.disableCopy)??Kn.disableCopy}),o=b(()=>{var g;return a.value?((g=s.value)==null?void 0:g.disableSelection)??Kn.disableSelection:!1}),r=b(()=>{var g;return a.value?((g=s.value)==null?void 0:g.maxLength)??Kn.maxLength:0}),c=b(()=>{var g;return((g=s.value)==null?void 0:g.triggerLength)??Kn.triggerLength}),p=()=>es?`${Ls(yn(es)?es:`https://${es}`)}${t.value.path}`:window.location.href,d=(g,v)=>{const{author:w,license:C,link:_}=n.value;return[g?w.replace(":author",g):"",v?C.replace(":license",v):"",_.replace(":link",p())].filter(T=>T).join(` +`)},h=()=>{if(me(t.value.copyright))return t.value.copyright.replace(":link",p());const{author:g,license:v}=t.value.copyright||{};return d(g??Kn.author,v??Kn.license)},m=g=>{const v=getSelection();if(v){const w=v.getRangeAt(0);if(a.value){const C=w.toString().length;if(l.value||r.value&&C>r.value)return g.preventDefault();if(C>=c.value){g.preventDefault();const _=h(),T=document.createElement("div");T.appendChild(v.getRangeAt(0).cloneContents()),g.clipboardData&&(g.clipboardData.setData("text/html",`${T.innerHTML} ${_.replace(/\\n/g,"`),g.clipboardData.setData("text/plain",`${v.getRangeAt(0).cloneContents().textContent||""} +------ +${_}`))}}}};se(()=>{const g=document.querySelector("#app");Pe(g,"copy",m),to(()=>{g.style.userSelect=o.value?"none":"auto"})})},wf=Ge({setup:()=>{yf()}}),gt=e=>{const n=atob(e);return hg(pg(dg(n,!0)))},Cf=e=>typeof e<"u",Ef=Array.isArray,xf=Object.entries,Pc=Object.keys,Vl=(e,...n)=>{if(n.length===0)return e;const t=n.shift()||null;return t&&xf(t).forEach(([a,s])=>{a==="__proto__"||a==="constructor"||(_n(e[a])&&_n(s)?Vl(e[a],s):Ef(s)?e[a]=[...s]:_n(s)?e[a]={...s}:e[a]=t[a])}),Vl(e,...n)},fs=()=>{const e=document.documentElement;return e.classList.contains("dark")||e.getAttribute("data-theme")==="dark"},Tf=(e,n)=>n==="json"?JSON.parse(e):new Function(`let config,__chart_js_config__; +{ +${e} +__chart_js_config__=config; +} +return __chart_js_config__;`)();var Sf=V({name:"ChartJS",props:{config:{type:String,required:!0},id:{type:String,required:!0},title:{type:String,default:""},type:{type:String,default:"json"}},setup(e){const n=ge(),t=ge(),a=q(!1),s=q(!0),l=b(()=>gt(e.config));let o=!1,r;const c=async p=>{const[{default:d}]=await Promise.all([u(()=>import("./auto-C0MMSKEI.js"),__vite__mapDeps([])),o?Promise.resolve():(o=!0,new Promise(g=>setTimeout(g,800)))]);d.defaults.borderColor=p?"#ccc":"#36A2EB",d.defaults.color=p?"#fff":"#000",d.defaults.maintainAspectRatio=!1;const h=Tf(l.value,e.type),m=t.value.getContext("2d");r==null||r.destroy(),r=new d(m,h),s.value=!1};return se(()=>{a.value=fs(),gc(document.documentElement,()=>{a.value=fs()},{attributeFilter:["class","data-theme"],attributes:!0}),oe(a,p=>c(p),{immediate:!0})}),()=>[e.title?i("div",{class:"chartjs-title"},decodeURIComponent(e.title)):null,s.value?i(bt,{class:"chartjs-loading",height:192}):null,i("div",{ref:n,class:"chartjs-wrapper",id:e.id,style:{display:s.value?"none":"block"}},i("canvas",{ref:t,height:400}))]}});const ns=Co("VUEPRESS_CODE_TAB_STORE",{});var Lf=V({name:"CodeTabs",props:{active:{type:Number,default:0},data:{type:Array,required:!0},id:{type:String,required:!0},tabId:{type:String,default:""}},slots:Object,setup(e,{slots:n}){const t=q(e.active),a=ge([]),s=()=>{e.tabId&&(ns.value[e.tabId]=e.data[t.value].id)},l=(p=t.value)=>{t.value=p
")}{t.value=p>0?p-1:a.value.length-1,a.value[t.value].focus()},r=(p,d)=>{p.key===" "||p.key==="Enter"?(p.preventDefault(),t.value=d):p.key==="ArrowRight"?(p.preventDefault(),l()):p.key==="ArrowLeft"&&(p.preventDefault(),o()),e.tabId&&(ns.value[e.tabId]=e.data[t.value].id)},c=()=>{if(e.tabId){const p=e.data.findIndex(({id:d})=>ns.value[e.tabId]===d);if(p!==-1)return p}return e.active};return se(()=>{t.value=c(),oe(()=>ns.value[e.tabId],(p,d)=>{if(e.tabId&&p!==d){const h=e.data.findIndex(({id:m})=>m===p);h!==-1&&(t.value=h)}})}),()=>e.data.length?i("div",{class:"vp-code-tabs"},[i("div",{class:"vp-code-tabs-nav",role:"tablist"},e.data.map(({id:p},d)=>{const h=d===t.value;return i("button",{type:"button",ref:m=>{m&&(a.value[d]=m)},class:["vp-code-tab-nav",{active:h}],role:"tab","aria-controls":`codetab-${e.id}-${d}`,"aria-selected":h,onClick:()=>{t.value=d,s()},onKeydown:m=>r(m,d)},n[`title${d}`]({value:p,isActive:h}))})),e.data.map(({id:p},d)=>{const h=d===t.value;return i("div",{class:["vp-code-tab",{active:h}],id:`codetab-${e.id}-${d}`,role:"tabpanel","aria-expanded":h},[i("div",{class:"vp-code-tab-title"},n[`title${d}`]({value:p,isActive:h})),n[`tab${d}`]({value:p,isActive:h})])})]):null}});const Ic=({active:e=!1},{slots:n})=>{var t;return i("div",{class:["code-group-item",{active:e}],"aria-selected":e},(t=n.default)==null?void 0:t.call(n))};Ic.displayName="CodeGroupItem";const Pf=V({name:"CodeGroup",slots:Object,setup(e,{slots:n}){const t=q(-1),a=ge([]),s=(r=t.value)=>{t.value=r {t.value=r>0?r-1:a.value.length-1,a.value[t.value].focus()},o=(r,c)=>{r.key===" "||r.key==="Enter"?(r.preventDefault(),t.value=c):r.key==="ArrowRight"?(r.preventDefault(),s(c)):r.key==="ArrowLeft"&&(r.preventDefault(),l(c))};return()=>{var c;const r=(((c=n.default)==null?void 0:c.call(n))||[]).filter(p=>p.type.name==="CodeGroupItem").map(p=>(p.props===null&&(p.props={}),p));return r.length===0?null:(t.value<0||t.value>r.length-1?(t.value=r.findIndex(p=>"active"in p.props),t.value===-1&&(t.value=0)):r.forEach((p,d)=>{p.props.active=d===t.value}),i("div",{class:"code-group"},[i("div",{class:"code-group-nav"},r.map((p,d)=>{const h=d===t.value;return i("button",{type:"button",ref:m=>{m&&(a.value[d]=m)},class:["code-group-nav-tab",{active:h}],"aria-pressed":h,"aria-expanded":h,onClick:()=>{t.value=d},onKeydown:m=>o(m,d)},p.props.title)})),r]))}}}),If='',Af='',Vf='';var Of={useBabel:!1,jsLib:[],cssLib:[],codepenLayout:"left",codepenEditors:"101",babel:"https://unpkg.com/@babel/standalone/babel.min.js",vue:"https://unpkg.com/vue/dist/vue.global.prod.js",react:"https://unpkg.com/react/umd/react.production.min.js",reactDOM:"https://unpkg.com/react-dom/umd/react-dom.production.min.js"};const pl=Of,di={html:{types:["html","slim","haml","md","markdown","vue"],map:{html:"none",vue:"none",md:"markdown"}},js:{types:["js","javascript","coffee","coffeescript","ts","typescript","ls","livescript"],map:{js:"none",javascript:"none",coffee:"coffeescript",ls:"livescript",ts:"typescript"}},css:{types:["css","less","sass","scss","stylus","styl"],map:{css:"none",styl:"stylus"}}},Df=(e,n,t)=>{const a=document.createElement(e);return _n(n)&&Pc(n).forEach(s=>{if(s.indexOf("data"))a[s]=n[s];else{const l=s.replace("data","");a.dataset[l]=n[s]}}),t&&t.forEach(s=>{a.appendChild(s)}),a},Lo=e=>({...pl,...e,jsLib:Array.from(new Set([...pl.jsLib||[],...e.jsLib||[]])),cssLib:Array.from(new Set([...pl.cssLib||[],...e.cssLib||[]]))}),Ot=(e,n)=>{if(Cf(e[n]))return e[n];const t=new Promise(a=>{var l;const s=document.createElement("script");s.src=n,(l=document.querySelector("body"))==null||l.appendChild(s),s.onload=()=>{a()}});return e[n]=t,t},Rf=(e,n)=>{if(n.css&&Array.from(e.childNodes).every(t=>t.nodeName!=="STYLE")){const t=Df("style",{innerHTML:n.css});e.appendChild(t)}},Hf=(e,n,t)=>{const a=t.getScript();if(a&&Array.from(n.childNodes).every(s=>s.nodeName!=="SCRIPT")){const s=document.createElement("script");s.appendChild(document.createTextNode(`{const document=window.document.querySelector('#${e} .vp-code-demo-display').shadowRoot; +${a}}`)),n.appendChild(s)}},Ff=e=>{const n=Pc(e),t={html:[],js:[],css:[],isLegal:!1};return["html","js","css"].forEach(a=>{const s=n.filter(l=>di[a].types.includes(l));if(s.length){const l=s[0];t[a]=[e[l].replace(/^\n|\n$/g,""),di[a].map[l]||l]}}),t.isLegal=(!t.html.length||t.html[1]==="none")&&(!t.js.length||t.js[1]==="none")&&(!t.css.length||t.css[1]==="none"),t},Ac=e=>e.replace(/
/g,"
").replace(/<((\S+)[^<]*?)\s+\/>/g,"<$1>$2>"),Vc=e=>`+${Ac(e)} +`,Nf=e=>`${e.replace("export default ","const $reactApp = ").replace(/App\.__style__(\s*)=(\s*)`([\s\S]*)?`/,"")}; +ReactDOM.createRoot(document.getElementById("app")).render(React.createElement($reactApp))`,jf=e=>e.replace(/export\s+default\s*\{(\n*[\s\S]*)\n*\}\s*;?$/u,"Vue.createApp({$1}).mount('#app')").replace(/export\s+default\s*define(Async)?Component\s*\(\s*\{(\n*[\s\S]*)\n*\}\s*\)\s*;?$/u,"Vue.createApp({$1}).mount('#app')").trim(),Oc=e=>`(function(exports){var module={};module.exports=exports;${e};return module.exports.__esModule?module.exports.default:module.exports;})({})`,Mf=(e,n)=>{const t=Lo(n),a=e.js[0]||"";return{...t,html:Ac(e.html[0]||""),js:a,css:e.css[0]||"",isLegal:e.isLegal,getScript:()=>{var s;return t.useBabel?((s=window.Babel.transform(a,{presets:["es2015"]}))==null?void 0:s.code)||"":a}}},$f=/([\s\S]+)<\/template>/u,Bf=/