원하는 입력값을 토대로 SealedSecret을 생성하여 yaml 파일 형식으로 제공해주는 프로그램을 만들어보고, 해당 프로그램을 사용하여 현재 제가 현업에서 어떻게 gitops 방식으로 secret을 git repository에서 관리하는지까지 알려드리겠습니다. 저는 간단한 단일 API Application을 만들 때 Express.js를 사용합니다.
SealedSecret에 대한 설치 및 CLI에서의 사용 방법은 이전글을 참고해주시기 바랍니다.
GitOps를 위한 Secret 관리하기 (2) - SealedSecrets 설치 및 사용하기 (CLI)
K8s Cluster 내부 및 Local PC 내부 각각에 SealedScrets 관련 소프트웨어를 설치하고, CLI를 통해 사용하는 방법을 알아보겠습니다. SealedSecrets의 개념은 이전 포스팅을 참고해 주세요. [Cloud Kubernetes 환경
min-nine.tistory.com
설계
저는 NCP에서 제공해주는 kubernetes Management System인 NKS를 사용합니다. 때문에 NCP에 접근할 수 있는 ncp-iam-authenticator 설치가 필요하고, NKS의 Prod, Dev 각각의 Cluster에 설치되어있는 Server-Side SealedSecret Contoller와 통신할 때 필요한 Client-Side Application인 kubeseal도 함께 설치가 필요합니다. 마지막으로 각각의 클러스터와 연동할 수 있는 kubeconfig.yaml 파일이 필요합니다.
때문에 저는 해당 Application을 Build하여 Container Image를 만들고, 해당 Container Image를 활용할 때 Argument 값에 따라 Cluster UUID값을 다르게 설정하여 ncp-iam-authenticator에서 kubeconfig 파일을 생성하여 환경변수에 설정해주는 형태로 설계하였습니다.
해당 어플리케이션의 이름은 secret-manager(가칭)라고 지었습니다.
secret-manager Application 구조
Express 설치 방법은 공식 docs에 자세히 안내되어 있습니다.
Express 설치
Learn how to install Express.js in your Node.js environment, including setting up your project directory and managing dependencies with npm.
expressjs.com
저는 공식문서에 나와있는 것과 똑같이 설치한 후에, 아래 파일들만 생성해주었습니다.
- Dockerfile - Application을 Conatiner로 만들기 위한 Build용도
- controllers/sealController.js - servcie.js에서 구현한 기능들을 활용
- services/sealService.js - 입력 데이터를 SealedSecret 관련 Yaml로 변환해주는 기능을 직접적으로 구현
- request-sealedSecet-example.http - 직접 http method를 호출하여 응답값을 확인하는 파일 (gitignore로 차단)
.
├── Dockerfile
├── README.md
├── bin
│ └── www
├── controllers
│ └── sealController.js
├── index.js
├── package-lock.json
├── package.json
├── public
│ └── stylesheets
│ └── style.css
├── request-sealedSecret-example.http
├── routes
│ ├── index.js
│ └── users.js
├── services
│ └── secretService.js
└── views
├── error.pug
├── index.pug
└── layout.pug
secret-manager Application Source
이제 직접적으로 코드를 구현해보도록 하겠습니다.
index.js
sealController를 require 받아 "/seal" 이라는 URI가 POST Method로 들어올 때 Controller를 바라볼 수 있도록 라우트를 등록해줍니다.
// index.js
const express = require("express");
const { sealController } = require("./controllers/sealController");
const app = express();
app.use(express.json());
// /seal 라우트 등록
app.post("/seal", sealController);
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`secret-manager is running on port ${PORT}`);
});
sealController.js
sealService에서 구현한 function들을 사용하고, 응답값을 retrun해줍니다. 저는 응답값을 그대로 복사하여 git repository에 등록해줄 것이기 때문에 text/plain으로 응답해줬습니다.
// controllers/sealController.js
const { generateBaseSecretYAML, sealSecret } = require("../services/secretService");
/**
* /seal : Key/Value를 받아 SealedSecret YAML 반환
*/
async function sealController(req, res) {
try {
const { secretName, namespace, data } = req.body;
if (!secretName || !namespace || !data) {
return res.status(400).json({
error: "secretName, namespace, data 필드가 필요합니다.",
});
}
// 1) 일반 Secret YAML 생성
const baseSecret = generateBaseSecretYAML(secretName, namespace, data);
// 2) SealedSecret 변환
const sealedSecretYAML = await sealSecret(baseSecret);
// 3) YAML로 응답 (text/plain)
res.setHeader("Content-Type", "text/plain");
return res.status(200).send(sealedSecretYAML);
// 만약 JSON으로 응답하려면:
// res.json({ sealedSecret: sealedSecretYAML });
} catch (error) {
console.error(error);
return res.status(500).json({ error: error.message || "서버 에러" });
}
}
module.exports = {
sealController,
};
secretService.js
사용자로부터 입력받은 평문 Value값을 base64로 인코딩하여 Kubernetes Secret Object를 생성할 수 있는 yaml 파일로 만드는 기능과, kubeseal 명령어를 사용하여 정말로 cluster 내부의 SealedSecret Controller와 통신하여 암호화된 sealedsecret.yaml파일을 응답해주는 기능을 구현합니다.
// services/secretService.js
const { spawn } = require("child_process");
/**
* 입력받은 Key/Value 데이터를 일반 Secret YAML로 변환 (base64 인코딩)
*/
function generateBaseSecretYAML(secretName, namespace, dataObj) {
const encodedData = Object.entries(dataObj).reduce((acc, [key, value]) => {
acc[key] = Buffer.from(value).toString("base64");
return acc;
}, {});
return `apiVersion: v1
kind: Secret
metadata:
name: ${secretName}
namespace: ${namespace}
type: Opaque
data:
${Object.entries(encodedData)
.map(([k, v]) => ` ${k}: ${v}`)
.join("\n")}
`;
}
/**
* kubeseal 명령어를 통해 Secret YAML -> SealedSecret YAML 변환
*/
function sealSecret(secretYAML) {
return new Promise((resolve, reject) => {
const kubeseal = spawn("kubeseal", [
"--format",
"yaml",
"--controller-name",
"sealed-secrets", // helm install시 지정한 이름
"--controller-namespace",
"default", // helm install시 지정한 namespace
]);
let stdoutData = "";
let stderrData = "";
kubeseal.stdout.on("data", (data) => {
stdoutData += data.toString();
});
kubeseal.stderr.on("data", (data) => {
stderrData += data.toString();
});
kubeseal.on("close", (code) => {
if (code === 0) {
resolve(stdoutData);
} else {
reject(stderrData);
}
});
kubeseal.stdin.write(secretYAML);
kubeseal.stdin.end();
});
}
// 서비스 메서드들을 export
module.exports = {
generateBaseSecretYAML,
sealSecret,
};
Dockerfile 작성
build stage에서는 kubeseal, ncp-iam-authenticator를 설치합니다.
# ---- 1) Build Stage ----
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install --production
COPY . .
# kubeseal 설치
RUN apk update && apk add --no-cache wget \
&& wget https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.20.2/kubeseal-0.20.2-linux-amd64.tar.gz \
&& tar -zxvf kubeseal-0.20.2-linux-amd64.tar.gz kubeseal \
&& mv kubeseal /usr/local/bin/kubeseal \
&& chmod +x /usr/local/bin/kubeseal \
&& rm kubeseal-0.20.2-linux-amd64.tar.gz
# ncp-iam-authenticator 설치
RUN apk update && apk add --no-cache curl libc6-compat \
&& curl -L -o /usr/local/bin/ncp-iam-authenticator \
https://github.com/NaverCloudPlatform/ncp-iam-authenticator/releases/latest/download/ncp-iam-authenticator_linux_amd64 \
&& chmod +x /usr/local/bin/ncp-iam-authenticator
runtime stage에서는 build stage에서 설치한 kubeseal, ncp-iam-authenticator를 가져오고, kubeconfig파일을 생성하여 환경변수로 등록해주는 작업을 진행합니다.
# ---- 2) Production Stage ----
FROM node:20-alpine
WORKDIR /app
# 빌드 시 전달받은 BUILD_ENV에 따라 cluster uuid를 분기 처리하기 위한 ARG 설정
ARG BUILD_ENV
ENV BUILD_ENV=${BUILD_ENV}
# NCLOUD 관련 빌드 인자
ARG NCLOUD_ACCESS_KEY
ARG NCLOUD_SECRET_KEY
ENV NCLOUD_ACCESS_KEY=${NCLOUD_ACCESS_KEY} \
NCLOUD_SECRET_KEY=${NCLOUD_SECRET_KEY} \
NCLOUD_API_GW=https://ncloud.apigw.ntruss.com
# builder 스테이지에서 필요한 파일들을 모두 복사
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /usr/local/bin/kubeseal /usr/local/bin/kubeseal
COPY --from=builder /usr/local/bin/ncp-iam-authenticator /usr/local/bin/ncp-iam-authenticator
COPY . .
# BUILD_ENV 값에 따라 CLUSTER_UUID를 결정하고 kubeconfig를 생성
RUN if [ "$BUILD_ENV" = "prod" ]; then \
CLUSTER_UUID="nks-prod-cluster-uuid"; \
else \
CLUSTER_UUID="nks-dev-cluster-uuid"; \
fi && \
echo "Using Cluster UUID: $CLUSTER_UUID" && \
ncp-iam-authenticator create-kubeconfig --region KR --clusterUuid "$CLUSTER_UUID" --output ./kubeconfig.yaml
ENV KUBECONFIG=/app/kubeconfig.yaml
EXPOSE 3000
CMD ["node", "index.js"]
Dockerfile에서 Build시 secret key, access key 등을 arguments로 활용하는것은 권장하지 않습니다만, 위 방법이 아니라면 미리 생성된 kubeconfig.yml 파일을 git repository로 등록하고 관리를 해줘야하기 때문에 권장하지 않는 방법을 사용하였습니다.
이제 dev 클러스터와 통신하는 secret-manager-dev 라는 컨테이너 이미지를 생성해줍니다.
docker build -t secret-manager-dev:0.1 \
--build-arg BUILD_ENV=dev \
--build-arg NCLOUD_ACCESS_KEY=${YOUR_NCLOUD_ACCESS_KEY} \
--build-arg NCLOUD_SECRET_KEY=${YOUR_NCLOUD_SECRET_KEY} \
-f Dockerfile .
빌드가 완료되면 위의 사진과 같이 sensetive한 내용은 build시 사용하지 말라는 경고문구가 함께 출력됩니다. 꼭 사용해야 할 이유가 있는지, 타당한 이유인지 한 번쯤 생각하고 사용해야 합니다.
빌드된 이미지를 실행시킵니다. 저는 dev 클러스터는 localhoast의 3000번 포트를, prod 클러스터는 3001번 포트로 접근하게 포트바인딩을 해줬습니다.
docker run -it --rm -p 3000:3000 secret-manager-dev:0.1
docker run -it --rm -p 3001:3000 secret-manager-prod:0.1
request-sealedSecret-example.htttp
http 파일을 생성하여 secret 오브젝트의 name과 namespace, 그리고 사용할 data를 아래와 같은 형식으로 입력하여 전송합니다.
### Mingyu-Api-Secret-Dev
POST http://localhost:3000/seal
Content-Type: application/json
{
"secretName": "mingyu-api-secret-dev",
"namespace": "test",
"data": {
"DBPASSWORD": "abcd0987!",
"SECRET_KEY": "testSecret!!",
"ACCESS_KEY": "testAcess@@"
}
}
그러면 아래와 같은 결과물을 확인할 수 있습니다. 저는 해당 결과물을 argocd와 연동시켜놓은 helm charts의 custom-values에 sealedsecret 태그를 만들어서 response로 출력된 text/plain의 metadata, spec을 그대로 복사하여 사용하고 있습니다.
댓글