Preface
Recently, I am learning how to use jenkins and Alibaba Cloud k8s to automatically integrate and deploy the springcloud microservice and front-end next.js project. Now I record it and share it with everyone. If there is anything wrong with the article, I hope the big guys can criticize and correct me.
DevOps CI/CD process
Object-oriented
- Familiar with vue or react
- Proficiency in using SpringCloud microservices
- Proficiency in using docker containers
- Proficiency in using jenkins automated operation and maintenance tools
- Familiar with k8s (deployment, service, ingress)
Ready to work
1. Purchase Alibaba Cloud ACK cluster (or build it
I bought the Alibaba Cloud ACK hosting version, create a cluster address
Note: There is no charge for creating an ACK cluster. The charge is for the NAT gateway, SLB load balancing, ECS server, etc.
2. Install gitlab
Here I wrote an article on Alibaba Cloud ECS to build gitlab, install gitlab address
3. Install jenkins
Here I wrote an article on Alibaba Cloud ECS to build jenkins, install jenkins address
4. Install docker
Use official website recommended yum installation is more convenient, the installation is as follows:
sudo yum install -y yum-utils \
device-mapper-persistent-data \
lvm2
sudo yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo
sudo yum install docker-ce docker-ce-cli containerd.io
system structure
Next.js front-end development
Next.js project:
服务:react+next.js
服务端口:3000,
K8S:Deployment+Server+Ingress
Pod名:demo-webapp
Dockerfile
FROM node:12
# 设置工作路径。所有的路径都会关联WORKDIR
WORKDIR /usr/src/app
# 安装依赖
COPY package*.json ./
RUN npm install
# 拷贝源文件
COPY . .
# 构建应用
RUN npm run build
# 运行应用
CMD [ "npm", "start" ]
SpringCloud microservice development
1. Service discovery: SpringCloud Eureka
微服务名:demo-eureka-server
微服务端口:8761,
K8S:Deployment+Service+Ingress(因为要通过网址查看服务注册情况,所以要添加ingress)
Pod名:demo-eureka-server
2. Service configuration: SpringCloud Config
微服务名:demo-config-server
微服务端口:8888,
K8S:Deployment+Service
Pod名:demo-config-server
3. Service authentication and authorization: SpringSecurity + Oauth2
微服务名:demo-auth-service
微服务端口:8901,
K8S:Deployment+Service
Pod名:demo-auth-service
4. Service gateway: SpringCloud Zuul
微服务名:demo-auth-service
微服务端口:5555,
K8S:Deployment+Service+Ingress(服务网关是所有服务对外唯一入口,所以要配置ingress)
Pod名:demo-auth-service
5. Write Dockerfile, K8S yaml
For convenience, the above microservice name and Pod name are both set to the same name. The specific service development process is temporarily ignored here. I will write an article on building enterprise-level microservices later when I have time. The directory structure of related microservices is as follows :
Note: Dockerfile and k8s yaml files are similar between different services. Let’s take Eureka as an example:
Eureka Dockerfile:
FROM openjdk:8-jdk-alpine
MAINTAINER "zhangwei"<zhangwei900808@126.com>
RUN mkdir -p /usr/local/configsvr
ARG JAR_FILE
ADD ${JAR_FILE} /usr/local/configsvr/app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/usr/local/configsvr/app.jar"]
EXPOSE 8888
Eureka K8S Yaml:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
nginx.ingress.kubernetes.io/configuration-snippet: |
rewrite ^/eureka/css/(.*)$ /eureka/eureka/css/$1 redirect;
rewrite ^/eureka/js/(.*)$ /eureka/eureka/js/$1 redirect;
nginx.ingress.kubernetes.io/force-ssl-redirect: 'true'
nginx.ingress.kubernetes.io/rewrite-target: /$2
nginx.ingress.kubernetes.io/service-weight: ''
generation: 4
name: <podname>-ingress
namespace: default
spec:
rules:
- host: baidu.com
http:
paths:
- backend:
serviceName: <podname>-svc
servicePort: 8761
path: /eureka(/|$)(.*)
pathType: ImplementationSpecific
---
apiVersion: v1
kind: Service
metadata:
name: <podname>-svc
namespace: default
spec:
externalTrafficPolicy: Local
ports:
- nodePort: 31061
port: 8761
protocol: TCP
targetPort: 8761
selector:
app: <podname>
sessionAffinity: None
type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
deployment.kubernetes.io/revision: '1'
generation: 1
labels:
app: <podname>
name: <podname>
namespace: default
spec:
progressDeadlineSeconds: 600
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
app: <podname>
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
labels:
app: <podname>
spec:
containers:
- env:
- name: LANG
value: C.UTF-8
- name: JAVA_HOME
value: /usr/lib/jvm/java-1.8-openjdk
image: <imagename>
imagePullPolicy: IfNotPresent
name: <podname>
ports:
- containerPort: 8761
protocol: TCP
resources:
requests:
cpu: 250m
memory: 512Mi
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
Note: Bootstrap.yml and application.yml files are different for different policy services, such as the following:
In demo-auth-service, demo-zuul-server configuration bootstrap.yml file add SpringCloud Config address:
spring:
application:
name: authservice
cloud:
config:
enabled: true
# config的Server名
uri: http://demo-config-server-svc:8888
Add the Eureka address to the demo-auth-service, demo-zuul-server, demo-config-server configuration application.yml file:
eureka:
instance:
preferIpAddress: true
hostname: demo-eureka-server-svc
client:
register-with-eureka: true
fetch-registry: true
service-url:
# eureka的Server名
defaultZone: http://demo-eureka-server-svc:8761/eureka/
Git workflow
According to the Git workflow, we can generally be divided into: development environment (develop), test environment (release), pre-production environment (uat) and production environment (prop), the corresponding branches are: dev, test, release, master, Therefore, the branches of the code submitted in different stages are not the same, and the dockerfile, jenkins pipline, and k8s yaml files are also different. Please pay attention to this.
Jenkins DevOps CI/CD
1. View specification
Jenkins can be added according to the git workflow: test view, pre-production view, production view, as shown below:
2. Create a pipeline task
We first create a pipeline task and write a pipline script script to create CI/CD pipeline steps
3. Write front-end pipline script
First write environment variables
environment {
GIT_REPOSITORY="前端代码仓库地址"
K8S_YAML="k8s yaml文件所在目录"
DOCKER_USERNAME="docker 仓库用户名"
DOCKER_PWD="docker仓库密码"
ALIYUN_DOCKER_HOST = '阿里云docker仓库域名'
ALIYUN_DOCKER_NAMESPACE="阿里云docker仓库命名空间"
ALIYUN_DOCKER_REPOSITORY_NAME="阿里云docker仓库命名空间下的仓库名"
}
Step 1: Clone the code
stage("Clone") {
steps {
echo "1.Clone Stage"
// 删除文件夹
deleteDir()
// 测试分支,jenkins-gitlab-ssh-hash是ssh密钥,替换成自己的就好
git branch: 'test', credentialsId: 'jenkins-gitlab-ssh-hash', url: "${GIT_REPOSITORY}"
script {
// 获取git提交的hash值做为docker镜像tag
GIT_TAG = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
// 组装成完整地址
DOCKER_REPOSITORY = "${ALIYUN_DOCKER_HOST}/${ALIYUN_DOCKER_NAMESPACE}/${ALIYUN_DOCKER_REPOSITORY_NAME}"
DOCKER_REPOSITORY_TAG = "${DOCKER_REPOSITORY}:${GIT_TAG}"
}
}
}
Step 2: Code Test
stage("Test") {
steps {
echo "2.Test Stage"
}
}
Step 3: Make docker image
stage("Build") {
steps {
echo "3.Build Docker Image Stage"
sh "docker build -t ${DOCKER_REPOSITORY_TAG} -f docker/Dockerfile ."
}
}
Step 4: Push the docker image
stage("Push") {
steps {
echo "4.Push Docker Image Stage"
//推送Docker镜像,username 跟 password 为 阿里云容器镜像服务的账号密码
sh "docker login --username=${DOCKER_USERNAME} --password=${DOCKER_PWD} ${ALIYUN_DOCKER_HOST}"
// 开始推送镜像到阿里云docker镜像仓库
sh "docker push ${DOCKER_REPOSITORY_TAG}"
// 删除jenkins生成的image
sh '''
docker images | grep seaurl | awk '{print $3}' | xargs docker rmi -f
'''
}
}
Step 5: k8s deploys docker image
stage("Deploy") {
steps {
echo "5.发布镜像"
// 使用sed替换k8s yaml文件中的<imagename>和<podname>
sh "sed -i 's#<imagename>#${DOCKER_REPOSITORY_TAG}#g;s#<podname>#${POD_NAME}#g' ${K8S_YAML}"
// 执行应用k8s yaml
sh "kubectl apply -f ${K8S_YAML}"
}
}
You can use Baidu for sed grammar yourself. Here I use # separation instead of / separation. The reason is that the separated character contains /, so it can't be used anymore.
First, let's take a look at the front-end k8s yaml file
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod-http01
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/force-ssl-redirect: 'true'
nginx.ingress.kubernetes.io/service-weight: ''
generation: 3
name: <podname>-ingress
namespace: default
spec:
rules:
- host: baidu.com
http:
paths:
- backend:
serviceName: <podname>-svc
servicePort: 3000
path: /
pathType: ImplementationSpecific
tls:
- hosts:
- baidu.com
secretName: <podname>-ingress
---
apiVersion: v1
kind: Service
metadata:
name: <podname>-svc
namespace: default
spec:
ports:
- port: 3000
protocol: TCP
targetPort: 3000
selector:
app: <podname>
sessionAffinity: None
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
deployment.kubernetes.io/revision: '1'
generation: 1
labels:
app: <podname>
name: <podname>
namespace: default
spec:
progressDeadlineSeconds: 600
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
app: <podname>
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
labels:
app:<podname>
spec:
containers:
- image: <imagename>
imagePullPolicy: IfNotPresent
name: <podname>
resources:
requests:
cpu: 250m
memory: 512Mi
The above yaml file contains: deployment, service, and ingress. Ingress is configured with tls, so you can use https to access the domain name, and configure nginx.ingress.kubernetes.io/force-ssl-redirect: 'true'
so you can automatically jump http to https, where <podname> and <imagename are included > Is to be replaced by sed with the real name and address.
complete pipline script script:
pipeline {
agent any
environment {
GIT_REPOSITORY="前端代码仓库地址"
K8S_YAML="k8s yaml文件所在目录"
DOCKER_USERNAME="docker 仓库用户名"
DOCKER_PWD="docker仓库密码"
ALIYUN_DOCKER_HOST = '阿里云docker仓库域名'
ALIYUN_DOCKER_NAMESPACE="阿里云docker仓库命名空间"
ALIYUN_DOCKER_REPOSITORY_NAME="阿里云docker仓库命名空间下的仓库名"
}
stages {
stage("Clone") {
steps {
echo "1.Clone Stage"
// 删除文件夹
deleteDir()
// 测试分支,jenkins-gitlab-ssh-hash是ssh密钥,替换成自己的就好
git branch: 'test', credentialsId: 'jenkins-gitlab-ssh-hash', url: "${GIT_REPOSITORY}"
script {
// 获取git提交的hash值做为docker镜像tag
GIT_TAG = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
// 组装成完整地址
DOCKER_REPOSITORY = "${ALIYUN_DOCKER_HOST}/${ALIYUN_DOCKER_NAMESPACE}/${ALIYUN_DOCKER_REPOSITORY_NAME}"
DOCKER_REPOSITORY_TAG = "${DOCKER_REPOSITORY}:${GIT_TAG}"
}
}
}
stage("Test") {
steps {
echo "2.Test Stage"
}
}
stage("Build") {
steps {
echo "3.Build Docker Image Stage"
sh "docker build -t ${DOCKER_REPOSITORY_TAG} -f docker/Dockerfile ."
}
}
stage("Push") {
steps {
echo "4.Push Docker Image Stage"
//推送Docker镜像,username 跟 password 为 阿里云容器镜像服务的账号密码
sh "docker login --username=${DOCKER_USERNAME} --password=${DOCKER_PWD} ${ALIYUN_DOCKER_HOST}"
// 开始推送镜像到阿里云docker镜像仓库
sh "docker push ${DOCKER_REPOSITORY_TAG}"
// 删除jenkins生成的image
sh '''
docker images | grep seaurl | awk '{print $3}' | xargs docker rmi -f
'''
}
}
stage("Deploy") {
steps {
echo "5.发布镜像"
// 使用sed替换k8s yaml文件中的<imagename>和<podname>
sh "sed -i 's#<imagename>#${DOCKER_REPOSITORY_TAG}#g;s#<podname>#${POD_NAME}#g' ${K8S_YAML}"
// 执行应用k8s yaml
sh "kubectl apply -f ${K8S_YAML}"
}
}
}
}
Click Build Now, as shown below:
4. Write a pipline script for microservices
First write environment variables
environment {
GIT_REPOSITORY="代码仓库地址"
MODULE_NAME="maven 模块名"
POD_NAME="k8s pod name"
K8S_YAML="${MODULE_NAME}/src/main/k8s/eurekasvr.yaml"
DOCKER_USERNAME="docker 仓库用户名"
DOCKER_PWD="docker仓库密码"
ALIYUN_DOCKER_HOST = 阿里云docker仓库域名'
ALIYUN_DOCKER_NAMESPACE="阿里云docker仓库命名空间"
ALIYUN_DOCKER_REPOSITORY_NAME="阿里云docker仓库命名空间下的仓库名"
}
Step 1: Clone the code
stage("Clone") {
steps {
echo "1.Clone Stage"
// 删除文件夹
deleteDir()
// 测试分支,jenkins-gitlab-ssh-hash是ssh密钥,替换成自己的就好
git branch: 'test', credentialsId: 'jenkins-gitlab-ssh-hash', url: "${GIT_REPOSITORY}"
script {
// 获取git提交的hash值做为docker镜像tag
GIT_TAG = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
// 组装成完整地址
DOCKER_REPOSITORY = "${ALIYUN_DOCKER_HOST}/${ALIYUN_DOCKER_NAMESPACE}/${ALIYUN_DOCKER_REPOSITORY_NAME}"
DOCKER_REPOSITORY_TAG = "${DOCKER_REPOSITORY}:${GIT_TAG}"
}
}
}
Step 2: Code test
stage("Test") {
steps {
echo "2.Test Stage"
}
}
Step 3: Make docker image
stage("Build") {
steps {
echo "3.Build Server"
sh "mvn -e -U -pl ${MODULE_NAME} -am clean package -Dmaven.test.skip=true dockerfile:build -Ddockerfile.tag=${GIT_TAG} -Ddockerfile.repository=${DOCKER_REPOSITORY}"
}
}
Here to explain, because we are using maven multi-modules, we need to package them in modules when compiling, so we need to use -pl to specify the module name, and then we use dockerfile-maven-plugin in pom.xml, as shown below:
<build>
<plugins>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>dockerfile-maven-plugin</artifactId>
<version>1.4.10</version>
<configuration>
<!-- 指定dockerfile所在目录-->
<dockerfile>src/main/docker/Dockerfile</dockerfile>
<buildArgs>
<!--提供参数向Dockerfile传递-->
<JAR_FILE>target/${project.build.finalName}.jar</JAR_FILE>
</buildArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
Step 4: Push the docker image
stage("Push") {
steps {
echo "4.Push Docker Image Stage"
//推送Docker镜像,username 跟 password 为 阿里云容器镜像服务的账号密码
sh "docker login --username=${DOCKER_USERNAME} --password=${DOCKER_PWD} ${ALIYUN_DOCKER_HOST}"
// 开始推送镜像到阿里云docker镜像仓库
sh "docker push ${DOCKER_REPOSITORY_TAG}"
// 删除jenkins生成的image
sh '''
docker images | grep seaurl | awk '{print $3}' | xargs docker rmi -f
'''
}
}
Step 5: k8s deploys docker image
stage("Deploy") {
steps {
echo "5.发布镜像"
// 使用sed替换k8s yaml文件中的<imagename>和<podname>
sh "sed -i 's#<imagename>#${DOCKER_REPOSITORY_TAG}#g;s#<podname>#${POD_NAME}#g' ${K8S_YAML}"
// 执行应用k8s yaml
sh "kubectl apply -f ${K8S_YAML}"
}
}
You can use Baidu for sed grammar yourself. Here I use # separation instead of / separation. The reason is that the separated character contains /, so it can't be used anymore.
First, let's take a look at the Eureka k8s yaml file
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
nginx.ingress.kubernetes.io/configuration-snippet: |
rewrite ^/eureka/css/(.*)$ /eureka/eureka/css/$1 redirect;
rewrite ^/eureka/js/(.*)$ /eureka/eureka/js/$1 redirect;
nginx.ingress.kubernetes.io/force-ssl-redirect: 'true'
nginx.ingress.kubernetes.io/rewrite-target: /$2
nginx.ingress.kubernetes.io/service-weight: ''
generation: 4
name: <podname>-ingress
namespace: default
spec:
rules:
- host: baidu.com
http:
paths:
- backend:
serviceName: <podname>-svc
servicePort: 8761
path: /eureka(/|$)(.*)
pathType: ImplementationSpecific
---
apiVersion: v1
kind: Service
metadata:
name: <podname>-svc
namespace: default
spec:
externalTrafficPolicy: Local
ports:
- nodePort: 31061
port: 8761
protocol: TCP
targetPort: 8761
selector:
app: <podname>
sessionAffinity: None
type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
deployment.kubernetes.io/revision: '1'
generation: 1
labels:
app: <podname>
name: <podname>
namespace: default
spec:
progressDeadlineSeconds: 600
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
app: <podname>
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
labels:
app: <podname>
spec:
containers:
- env:
- name: LANG
value: C.UTF-8
- name: JAVA_HOME
value: /usr/lib/jvm/java-1.8-openjdk
image: <imagename>
imagePullPolicy: IfNotPresent
name: <podname>
ports:
- containerPort: 8761
protocol: TCP
resources:
requests:
cpu: 250m
memory: 512Mi
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
The above yaml file contains: deployment, service and ingress. Ingress is configured with tls, so you can use https to access the domain name, and is configured with nginx.ingress.kubernetes.io/force-ssl-redirect: 'true'
so you can automatically jump http to https, where <podname> and <imagename are included > Is to be replaced by sed with the real name and address.
complete pipline script script:
pipeline {
agent any
environment {
GIT_REPOSITORY="代码仓库地址"
MODULE_NAME="maven 模块名"
POD_NAME="k8s pod name"
K8S_YAML="${MODULE_NAME}/src/main/k8s/eurekasvr.yaml"
DOCKER_USERNAME="docker 仓库用户名"
DOCKER_PWD="docker仓库密码"
ALIYUN_DOCKER_HOST = 阿里云docker仓库域名'
ALIYUN_DOCKER_NAMESPACE="阿里云docker仓库命名空间"
ALIYUN_DOCKER_REPOSITORY_NAME="阿里云docker仓库命名空间下的仓库名"
}
stages {
stage("Clone") {
steps {
echo "1.Clone Stage"
// 删除文件夹
deleteDir()
git branch: 'test',credentialsId: '1297dda3-e592-4e70-8fb0-087a26c08db0', url: "${GIT_REPOSITORY}"
script {
// 获取git代码tag为docker仓库tag
// GIT_TAG = sh(returnStdout: true,script: 'git describe --tags --always').trim()
// 获取git提交hash做为docker仓库tag
GIT_TAG = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
DOCKER_REPOSITORY = "${ALIYUN_DOCKER_HOST}/${ALIYUN_DOCKER_NAMESPACE}/${ALIYUN_DOCKER_REPOSITORY_NAME}"
// docker 阿里镜像仓库
DOCKER_REPOSITORY_TAG = "${DOCKER_REPOSITORY}:${GIT_TAG}"
}
}
}
stage("Test") {
steps {
echo "2.Test Stage"
}
}
stage("Build") {
steps {
echo "3.Build Server"
sh "mvn -e -U -pl ${MODULE_NAME} -am clean package -Dmaven.test.skip=true dockerfile:build -Ddockerfile.tag=${GIT_TAG} -Ddockerfile.repository=${DOCKER_REPOSITORY}"
}
}
stage("Push") {
steps {
echo "4.Push Docker Image Stage"
//推送Docker镜像,username 跟 password 为 阿里云容器镜像服务的账号密码
sh "docker login --username=${DOCKER_USERNAME} --password=${DOCKER_PWD} ${ALIYUN_DOCKER_HOST}"
// 开始推送镜像到阿里云docker镜像仓库
sh "docker push ${DOCKER_REPOSITORY_TAG}"
// 删除jenkins生成的image
sh '''
docker images | grep seaurl | awk '{print $3}' | xargs docker rmi -f
'''
}
}
stage("Deploy") {
steps {
echo "5.发布镜像"
sh "sed -i 's#<dockerrepository>#${DOCKER_REPOSITORY_TAG}#g;s#<podname>#${POD_NAME}#g' ${K8S_YAML}"
sh "kubectl apply -f ${K8S_YAML}"
}
}
}
}
Click Build Now, as shown below:
Check whether k8s is successful
You can pass the command
kubectl get deploy
kubectl get pod
kubectl get svc
kubectl get ingress
You can also view the pod log to analyze its success or failure:
kubectl logs podname
Visit Eureka
Postman access microservice interface address
You can access the external zuul address through the postman interface to see if it can be authenticated:
Browser access web page address
You can check whether the deployment is successful through the deployment address of next.js:
to sum up
1. Jenkins pipline script and Dockerfile are written in various ways on the Internet. Just find one that meets the standard.
2. The function of kubectl apply -f is: if deployment is not created, it will be created, otherwise it will be updated
3. The Service in the microservice k8s yaml file should be set to type: NodePort otherwise, the microservices cannot communicate
4. Jenkins uses -pl and dockerfile-maven-plugin to compile the maven module of microservices
5. If you want to use k8s ingress for microservices, you need to set https, and http will automatically jump to https
Quote
JenkinsPipeline deploys a Kubernetes application
uses jenkins pipeline to automatically build and deploy to k8s
Configuring-CI-CD-on-Kubernetes-with-Jenkins
spring-k8s
jenkins pipeline is automatically built and deployed to k8s
combat (1) Microservice infrastructure based on OAUTH2.0 unified authentication and authorization
Use cert-manager to apply for a free HTTPS certificate
SPRINGBOOT uses SPRING.PROFILES.ACTIVE=@SPRING.ACTIVE@ to switch configuration files flexibly in different environments
https://kuboard.cn/learning/k8s-practice/ocp/eureka-server.html#%E6%9F%A5%E7%9C%8B%E9%83%A8%E7%BD%B2%E7%BB%93%E6%9E%9C
kubernetes simple example of
k8s-nginx-ingress eureka secondary path forwarding problem
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。