对项目重构时有这样一个需求,1)要把代码库某个目录下的所有代码作为一个新代码库的根目录,2)并且之前所有的代码提交记录要一并迁移到这个新的git repo。
当你尝试用
git filter-branch --subdirectory-filter YOUR_SUB_DIR -- --all
来解决问题时,会看到一个警告推荐我们使用 git
filter-repo
。它是一个用于重写git history的多功能小工具,用法参考
filter-repo使用手册
。我们的需求在这里只是它的一个小case。
下面直接列出操作步骤:
brew install git-filter-repo
2)Clone 原来的Repo
mkdir codebase cd codebase git clone YOUR_GIT_REPO_URL/myProject cd myProject
3) 拉取所有信息到本地
git fetch --all git pull --all
4)执行 filter-repo 命令,让某个子目录成为新repo的根目录。
git filter-repo --subdirectory-filter The_SubDir_in_myProject
5) 在github/gitlab创建一个新repo,把这个repo设为这个子目录的remote目标
git remote add origin YOUR_NEW_REPO_GIT_URL
6) 把master的history push到新repo
git branch -M master git push -uf origin master
7)把所有branchs/tags都push上去
git push --all origin
SLF4J 是什么?
如何使用SLF4J?
SpringBoot底层默认支持logbak,如果使用log4j2则需要显示的exclude logback。
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency> <dpendencies>
关于SLF4J的MDC
关于log4j2自己的log。
- 如果我们希望看到log4j2自己的log,可以设置-Dlog4j2.debug=true,让它自己的log显示在console。
- Composite Configuration, 因为可以通过 log4j2.configurationFile 设置多个逗号分隔的配置文件,log4j有一套自己的逻辑对多个配置文件进行merge。所以除非项目中有特定的需求,显示的通过log4j2.configurationFile显示的指定配置文件可以减少不必要的困惑。
在连接上一个 K8S cluster 后执行下面的命令可以看到系统中的ingressclasses。这篇文字用来帮助自己理解下面几行简单的输出。
╰─$ kubectl get ingressclass NAME CONTROLLER PARAMETERS AGE awslb ingress.k8s.aws/alb IngressClassParams.elbv2.k8s.aws/alb 20d nginx nginx.org/ingress-controller <none> 30d os-nginx k8s.io/ingress-nginx <none> 30d
Mental Model
在Kubernets里经常会提到Pod,Service,Ingress,Ingress Controller, Ingress Class,那他们之间有什么逻辑关系呢?
Pod
Service
Ingress / Ingress Controller / Ingerss Class
既然“ingress“的核心功能就是7层路由/反向代理,那么借助早已存在的Nginx、HAProxy等产品实现IngressController就是很自然的想法了。另一个ingress controller的实现类别可以划分到service mesh阵营,比如Istio Ingerss、Gloo等。
k8s官网列出的一些Ingerss Controller实现。
而这篇文章详细讲解了各种Ingress Controller的特性以方便我们根据自己项目的需求做出选择。直接贴上文章的干货图片:在一个Kubernets集群里可以定义多个不同Ingress Controller实现/类型,那么Ingress对象如何知道自己的数据是提供给哪个Ingress Controller的呢?
在Kubernetes 1.18之前,是通过在Ingress的一个annotation
kubernets.io/ingress.class
来声明。
在Kubernetes 1.18正式引入了一个新的k8s资源对象 IngressClass 来帮助Ingress定义它绑定到哪个IngressController。
下面是一个官网的IngressClass对象定义示例,spec.controll
定义了IngressController的实现,spec.parameters
相当于定义了你可以在Ingerss对象里可以向这个IngressController对象能够传递的参数,当然这些参数也是这种IngressControll必须支持的。不同的Ingress Controller实现其需要的parameter肯定是不同的,而k8s 1.18之前通过annoation给IngerssController传递参数的方式就显得比较随意无章可循了,这应该也是IngressClass出现的一个原因。apiVersion: networking.k8s.io/v1 kind: IngressClass metadata: name: external-lb spec: controller: example.com/ingress-controller parameters: apiGroup: k8s.example.com kind: IngressParameters name: external-lb
有了IngressClass,那么在Ingress中只要设置
spec.ingressClassName
为某个IngerssClass的名字,那么就意味着这个Ingress的配置就会被这个IngerssClass所对应的IngressController所获取并被这个IngressControll生成为对应的路由rules,从而完成把一个集群外请求路由到Service的功能。以上就是关于Kubernetes里Ingerss的几个基本概念。
有关Nginx的IngressController
基于Nginx实现的IngressController分为
Kubernets社区版
和Nginx版
。Kubernets社区版由Kubernetes社区和F5 Nginx工程师基于开源的Nginx实现,其官网 code doc。
Nginx版自己又分为免费的基于开源Nginx的IngressController实现和商业版。Nginx开源版code doc。
所以就开源的版本来说,一个是Kubernets社区版,一个是Nginx开源版,两个都是基于开源的Nginx实现的,只是owner不通。表现在IngressClass的定义中,就是字段
spec.controller
的值一个是Kubernets社区版的k8s.io/ingress-nginx
, 一个是nginx开源版的nginx.org/ingress-controller
下面这个表格列出了Nginx Ingress Controller的 Kubernets社区版和Nginx开源版的区别。可以看到,两者差别不大,k8s社区版功能略好于Nginx开源版。而Nginx开源版因为没有使用Lua性能又好于k8s社区版。
Aspect or Feature kubernetes/ingress-nginx nginxinc/kubernetes-ingress with NGINX nginxinc/kubernetes-ingress with NGINX Plus Fundamental NGINX version Custom NGINX build that includes several third-party modules NGINX official mainline build NGINX Plus Commercial support Included Implemented in Go/Lua (while Nginx is written in C) Go/Python Go/Python Load balancing configuration via the Ingress resource HTTP load balancing extensions - Annotations See the supported annotations See the supported annotations See the supported annotations HTTP load balancing extensions – ConfigMap See the supported ConfigMap keys See the supported ConfigMap keys See the supported ConfigMap keys TCP/UDP Supported via a ConfigMap Supported via custom resources Supported via custom resources Websocket Supported Supported via an annotation Supported via an annotation TCP SSL Passthrough Supported via a ConfigMap Supported via custom resources Supported via custom resources JWT validation Not supported Not supported Supported Session persistence Supported via a third-party module Not supported Supported Canary testing (by header, cookie, weight) Supported via annotations Supported via custom resources Supported via custom resources Configuration templates See the template See the templates See the templates Load balancing configuration via Custom Resources Not supported See VirtualServer and VirtualServerRoute resources See VirtualServer and VirtualServerRoute resources TCP/UDP load balancing Not supported See TransportServer resource See TransportServer resource TCP SSL Passthrough load balancing Not supported See TransportServer resource See TransportServer resource Deployment TLS certificate and key for the default server Required as a command-line argument/ auto-generated Required as a command-line argument Required as a command-line argument Helm chart Supported Supported Supported Operator Not supported Supported Supported Operational Reporting the IP address(es) of the Ingress controller into Ingress resources Supported Supported Supported Extended Status Supported via a third-party module Not supported Supported Prometheus Integration Supported Supported Supported Dynamic reconfiguration of endpoints (no configuration reloading) Supported with a third-party Lua module Not supported Supported再回到文章开头的命令输出,是不是看到的更多了些?
╰─$ kubectl get ingressclass NAME CONTROLLER PARAMETERS AGE awslb ingress.k8s.aws/alb IngressClassParams.elbv2.k8s.aws/alb 20d nginx nginx.org/ingress-controller <none> 30d os-nginx k8s.io/ingress-nginx <none> 30d
References:
[1]: Ingress
[2]: IngressController
[3]: IngressClass
[4]: Comparing Ingress Controllers for Kubernetes
[5]: 基于Nginx的Ingress Controller在社区和商业版之间的比较
[6]: Kubernetes社区版
[7]: Nginx开源版
[8]: Nginx Ingress Controll社区版和Nginx开源版的比较1. 根据什么数据进行授权
认证成功后以GrantedAuthority 的形式保存在Authentication对象中的authorities会别用来进行授权运算。
请求被 AuthenticationManager 认证之后,其 Principal 的 authorities 以一组 GrantedAuthority 的形式被保存在 Authentication 对象里。
public interface GrantedAuthority extends Serializable { String getAuthority();
在是否可以对security object (比如一个方法的调用、一个web request的处理)进行访问之前,需要使用 AuthorizationManager 来决定是否可以invoke 这些security objects。
如果这些授权不能以String的形式表达出来,那么就用返回null的形式告诉 AuthorizationManager/AccessDecisionManager 需要自己进行特殊的处理。 Spring Security 源码里就有一个这样的例子,请参阅: WebExpressionConfigAttribute, WebExpressionVoter。
2. 通过 AuthorizationManager 体会设计的改进
AuthorizationManager 是在 spring security 5.5 中被加入。从spring-security-core-6.0开始,AccessDecisionManager AccessDecisionVoter 已经被 deprecated,由 AuthorizationManager 取代其作用。 对于之前定制化AccessDecisionManager AccessDecisionVoter的代码应该考虑迁移到AuthorizationManager。
AuthorizationManager
AccessDecisionManager
AccessDecisionVoter
- 之前AccessDecisionManager 通过抛出异常,现在default的verify也是通过exception。
- 明确返回AuthorizationDecision来标识。
- 之前有support方法,跟AuthenticationProvider的思路很像。
从方法签名可以直接看出上面这几点。官方文档列出了更有意义的变化:- AuthorizationManager的API相对于之前FilterSecurityInterCepter/AccessDecisionManager要使用metadata sources, config attributes, decison managers, voters进行授权判断要简化很多。
- 因为通过把AuthenticationManger放在Supplier中实现了对认证数据的延迟访问,这对一些无需认证的授权是有好处的。
- 支持基于Bean的配置。
3. AuthorizationManagers/AuthorizationFilter 以及 AccessDecisionManager/FilterSecurityInterceptor
... ... SessionManagementFilter ExceptionTranslationFilter FilterSecurityInterceptor SwitchUserFilter
4. 如何在 Security Filters 中使用AuthorizationFilter 或 FilterSecurityInterceptor?
@Bean SecurityFilterChain web(HttpSecurity http) throws AuthenticationException { .authorizeHttpRequests((authorize) -> authorize .anyRequest().authenticated(); // ... return http.build();
5. 详细的使用范式
关于AuthorizationFilter/AuthorizationManger,可参考:https://docs.spring.io/spring-security/reference/servlet/authorization/authorize-http-requests.html
关于 FilterSecurityInterceptor,可参考: https://docs.spring.io/spring-security/reference/servlet/authorization/authorize-requests.html
References:
[1]: https://docs.spring.io/spring-security/reference/servlet/authorization/index.html关于Spring Security里的Authentication,官方文档总结的不错。理解这些classes的作用与关系是正确使用Spring Security Authentication的前提。
认证的方式不同,认证逻辑就不同,这样每个认证方式都会有对应的fitler实现。执行认证的大致流程以 AbstractAuthenticationProcessingFilter 为例描述一下。不同类别的 Authentication Filter 的处理略有差异,但大体逻辑差不多:
- Authentication Filter 接收请求 http request。
- 从request中获取凭证(credential)等数据,封装在Authentication对象中,比如:OAuth2LoginAuthenticationToken, UsernamePasswordAuthenticationToken等。
- 用AuthenticationManager向对其传入的Authentication 进行实际的认证工作。
- 认证成功的处理,比如保存设置了授权信息的Authentication到SecurityContext中。
- 失败进行处理。
1) Authentication Filter 接收请求
1)继承自 AbstractAuthenticationProcessingFilter 的authentication fitler class 有3个。
2)继承自 AbstractPreAuthenticatedProcessingFilter 的类有5个。
3)其它分别直接继承 OncePerRequestFilter 和 GenericFilterBean,比如:
OncePerRequestFilter 较 GenericFilterBean可以保证只被filters处理一次。
public class BearerTokenAuthenticationFilter extends OncePerRequestFilter public class BasicAuthenticationFilter extends OncePerRequestFilter public class AnonymousAuthenticationFilter extends GenericFilterBean public class RememberMeAuthenticationFilter extends GenericFilterBean
2)Request to Authentication
3)AuthenticationManager
在认证后,principal的授权信息会被写在authentication对象的authorities字段。下图摘自《Spring Security in Action》,使用username password做认证。
如上图所示的,具体的认证工作是委托给AuthenticationProvider完成的。在Spring Security的代码实现中,也并不是由AuthenticationManager直接包含一组AuthenticationProvider的方式完成,中间还有一个叫做ProviderManager的类,下面列出它的两个关键字段体会下。
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean { private List<AuthenticationProvider> providers = Collections.emptyList(); private AuthenticationManager parent; ... ...
可以看到ProviderManager包含的不是一个provider而是a list of providers。通过提供一组providers就向用户提供了更多灵活控制的可能性。当然随之而来的就是这里就需要明确定义providers的认证结果以谁为准的规则。源码authenticate()的doc说得很明白:
Attempts to authenticate the passed Authentication object.
The list of AuthenticationProviders will be successively tried until an AuthenticationProvider indicates it is capable of authenticating the type of Authentication object passed. Authentication will then be attempted with that AuthenticationProvider.
If more than one AuthenticationProvider supports the passed Authentication object, the first one able to successfully authenticate the Authentication object determines the result, overriding any possible AuthenticationException thrown by earlier supporting AuthenticationProviders. On successful authentication, no subsequent AuthenticationProviders will be tried. If authentication was not successful by any supporting AuthenticationProvider the last thrown AuthenticationException will be rethrown.让我们对比一下AuthenticationManger和AuthenticationProvider这两个interface的定义。看了下面的定义,你会不会问一个问题:既然两个接口有一个一摸一样的方法authentication(),为什么不让AuthenticationProvider继承AuthenticationManager? 我想或许是为了明确两个类的职责吧。
以图形的方式看看它们的关系:
如果我们要实现某个特殊的在Spring里没有的认证方式,我们就需要实现自定的AuthenticationProvider并通过覆盖WebSecurityConfigurerAdapter里的configure()方法实现。
@Configuration public class ProjectConfig extends WebSecurityConfigurerAdapter { @Autowired private AuthenticationProvider authenticationProvider; @Override protected void configure(AuthenticationManagerBuilder auth) { auth.authenticationProvider(authenticationProvider);
不过这个类已经被官方API文档标为Deprecated,并推荐使用HttpSecurity定义SecurityFilterChain的方式或者通过WebSecurityCustomizer来配置WebSecurity。参考源码:
到这里我们应该已经知道具体的认证逻辑都在AuthenticationProvider里。想知道Spring Security提供了哪些开箱即用的provider吗?见下图,一共17个。
再捋一下与认证相关的类,就结束这篇吧。虽然没有涉及过多细节,相信理解了这些脉略应该也能在copy-past代码的时候点点头了。。。
SecurityContextHolder:保存SecurityContext的地方。
SecurityContextHolderStrategy:定义SecurityContext在线程中共享的策略模式。如果要跨线越Spring管理的线程,请参考 。。。。
SecurityContext - 认证成功后Authentication对象就放在这里。
Authentication - 存放要认证的信息以及被 AuthenticationManager 认证后的结果,认证成功后被放入SecurityContext。
GrantedAuthority - 请求认证的principal认证成功后被赋予的权限(i.e. roles, scopes, etc.)
AuthenticationManager - authentication相关的filter调用这个对象做认证。
AbstractAuthenticationProcessingFilter:各个认证相关filter的父类。
ProviderManager - AuthenticationManager的一个实现.
AuthenticationProvider - 由ProviderManager 用来做具体的认证。
AuthenticationEntryPoint: 用于询问并接收用户的credentials,可以是重定向到一个网页或者发送http WWW-Authenticate response。我想现在我们看到下面这些类时,就应该能够大致知道/理解他们在Spring Security Authentication类图里的位置了吧?
UserDetails, User
UserDetailsService, UserDetailsManager, JdbcUserDetailsManager
PasswordEncoder
如果没有,一定是我还没描述清楚。
References:
[1]: https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html
[2]: 《Spring Security in Action》在项目中实际使用Spring Security时,我们的大部分工作其实都是配置HttpSecurity。要么通过spring的 http xml element 来配置,要么通过配置类里的HttpSecurity class来配置,所以在理解了DelegatingFilterProxy,FilterChainProxy,SecurityFilterChain之间的关系之后就很有必要了解一下HttpSecurity类了。
HttpSecurity这个类的名称与它的实际功用相差甚远,其实把它称为HttpSecurityFilterChainBuiler应该更合适,因为它的作用就是利用构造器模式构造出SecurityFilterChain的一个实例供FilterChainProxy使用。这点从它的类签名就能看出来。
如果有多个 SecurityFilterChain 被配置、构造出来,它们的顺序可以通过注解 @Order来设定。没有@Order注解的优先级最低。同一order层级的,就可以通过 SecurityFilterChain 中的RequestMatcher 来决定了该chain是否与http request匹配了。我们应该尽量把特殊的匹配放在前面,通用的放在后面。
1) 体会下HttpSecurity源码的定义部分:
2) 用xml配置http security:
虽然现在基于Spring的开发都是基于注解的了,但是如果遇到遗留系统里通过http元素来定义HttpSecurity,那么俯视一下下面的schema应该也能大致了然了。。。
3) fitlers 的顺序定义:
4) HttpSecurity build filter的套路
看看与 authentication 相关的两个fitler的构建。
1)从两个filter看规律
public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception { return getOrApply(new FormLoginConfigurer<>()); public HttpBasicConfigurer<HttpSecurity> httpBasic() throws Exception { return getOrApply(new HttpBasicConfigurer<>());
FormLoginConfigurer 是 AbstractAuthenticationFilterConfigurer 的子类, HttpBasicConfigurer是AbstractHttpConfigurer。这是因为basic的认证方式比起form形式的认证要简单得多。
public final class FormLoginConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractAuthenticationFilterConfigurer<H, FormLoginConfigurer<H>, UsernamePasswordAuthenticationFilter> { public abstract class AbstractAuthenticationFilterConfigurer<B extends HttpSecurityBuilder<B>, T extends AbstractAuthenticationFilterConfigurer<B, T, F>, F extends AbstractAuthenticationProcessingFilter> extends AbstractHttpConfigurer<T, B> { public final class HttpBasicConfigurer<B extends HttpSecurityBuilder<B>> extends AbstractHttpConfigurer<HttpBasicConfigurer<B>, B> {
负责根据这些configuer构造出对象来的类是AbstractConfiguredSecurityBuilder。
public abstract class AbstractConfiguredSecurityBuilder<O, B extends SecurityBuilder<O>> extends AbstractSecurityBuilder<O> {
2) 没有被使用的AuthenticationFilter
BasicAuthenticationFilter
UsernamePasswordAuthenticationFitler
AuthenticationFilter
References:
[1]: https://docs.spring.io/spring-security/reference/servlet/configuration/java.html#jc-httpsecurity
[2]: https://docs.spring.io/spring-security/reference/servlet/configuration/java.html#_multiple_httpsecurity
[3]: https://docs.spring.io/spring-security/reference/servlet/configuration/java.html#jc-custom-dsls
[4]: https://www.baeldung.com/spring-onceperrequestfilter这段文字主要源于对 https://docs.spring.io/spring-security/reference/servlet/architecture.html 的学习和理解,其实就是对下图的理解。
上图表达了下面几个类之间的关系:
DelegatingFilterProxy, FilterChainProxy (springSecurityFilterChain), SecurityFilterChain (security filter)
通过调用 SecurityFilterChain API 把 Security Filters 组装成一个或多个chain,再设置给FilterChainProxy使用。对于下图这样FilterChainProxy有多个SecurityFilterChain的情况,只会触发第一个匹配的securityFilterChain。
1)使用 Spring Security
不使用Spring Boot的情况下,就需要自己在web.xml文件中定义springSecurityFilterChain。
<filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
2)入口 DelegatingFilterProxy
下面通过源码简单看下DelegatingFilterProxy实例化的过程。
Tomcat启动时会在 web 容器中初始化 DelegatingFilterProxy 实例,DelegatingFilterProxy 本身既是一个Filter也是一个ServletContextAware的实例。 Spring 使用 ContextLoaderListener 来加载spring的bean。org.springframework.web.context.support.GenericWebApplicationContext 则是servlet context和spring context真正交汇的地方。
从下面DelegatingFilterProxy实现的接口就可以感知到 DelegatingFilterProxy 最关键的作用就是作为Servlet Container 和 Spring Context的桥梁。
因为Spring要等web context初始化完成才能初始化自己的context,所以在spring中定义的filter beans就要延迟初始化才行。通过延迟初始化就解决了Filter必须定义在Servlet Container中的问题。 Spring很巧妙的通过FilterChain接口把这些filter beans串在一起public class DelegatingFilterProxy extends GenericFilterBean
而 ServletContextAware 是Spring的一个接口。
public abstract class GenericFilterBean implements Filter, BeanNameAware, EnvironmentAware, EnvironmentCapable, ServletContextAware, InitializingBean, DisposableBean
从DelegatingFilterProxy的两个关键fields:WebApplicationContext webApplicationContext 和 Filter delegate,也能知道DelegatingFilterProxy桥梁作用。
上图中的delegate是FilterChainProxy的实例。
3) FilterChainProxy 包装了 filers。
4)springSecurityFilterChain 进入 DelegatingFilterProxy
至此,应该对文首的第一个图能说出点儿什么了吧… …
DelegatingFilterProxy
的 field ‘Filter delegate’ 就是名为springSecurityFilterChain 的 FilterChainProxy实例
。
而springSecurityFilterChain默认包含16个SecurityFilterChain
。Reference:
[1]: https://www.baeldung.com/spring-web-contexts
[2]: https://docs.spring.io/spring-security/reference/servlet/architecture.html
[3]: https://docs.spring.io/spring-security/reference/servlet/architecture.html#servlet-security-filters项目中遇到多个微服务调用需要考虑和处理某个环节失败时的处理。虽然这里不需要很强的事务概念,但是需要对失败的动作进行重试等操作。这里的重试本质上就是rollback的另一种形式,在saga里算是“forward recovery”。
借机又翻看了一下相关的文章,贴到了文末。Saga vs TCC
- Saga相比TCC的缺点是缺少预留动作,所以某些情况补偿的实现比较麻烦甚至无法撤销只能补救。不过没有预留动作也意味着不必担心资源释放的问题。
- TCC最少通信次数为2n,Saga为n(n=sub-transaction的数量)。
- 第三方服务没需要提供有Try接口。
总体感觉下来SAGA更适合微服务的多数场景。Simple Saga
从这个示意图其实可以看作是Chris演讲中提到的最最原始的模式。可以把callback看作是saga事务参与方发送消息到message broker。而调用链的第一个节点就充当了saga的协调者。
各个微服务的updateStatus端点就是message的listner,只不过这里直接通过callback实现而没有利用消息队列。
最开始的endpoint负责生成一个transactionId并依次传递给每个下游服务,每个下游服务通过callback把自己的状态更新给上游。getStatus() 端点提供给UI获取当前状态。
transCheckAndAmend(trans_n) 每个服务暴露的业务方法都需要提供一个补偿方法。
服务的入口方法其实充当了协调者, 更像orchestration的,而不是choreography的。
Timer 是个后台定时器不停的检查服务状态,如果状态不成功就调用compensating endpoint.
Reference:
[1]: Saga的经典论文 https://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf
[2]: 《Microservice Pattern》”Chapter4, Managing transactions with sagas”
[3]: Chris Richardson 在2017年的演讲:https://www.youtube.com/watch?v=YPbGW3Fnmbc一些中文网文:
[3] 分布式事务:Saga模式 https://www.jianshu.com/p/e4b662407c66
[4] 七种分布式事务的解决方案 https://cloud.tencent.com/developer/article/1806989
[5] 分布式事务六种解决方案 https://zhuanlan.zhihu.com/p/183753774本文基本是基于此efs workshop的记录和扩展。
要创建一个EFS资源,大致有以下几个步骤:
要在哪个VPC上创建 –> 这个VPC上子网的CIDR
创建一个SG –> 设置这个SG的ingress rule: 对子网开放NFS的2049
创建EFS,根据需求设置不同的参数比如是否加密、备份、performance mode、throughput-mode 等。
找到VPC上的public subnet,在这些public subnet上创建Moint Target。有了 mount targets,这个NFS就已经可以对外提供服务了。
如果需要对mount的网络文件系统的目录设置特定的user、group属性,那么可以通过在这个NFS上创建 Access Points 完成。因为EFS是可以跨region在这个region的所有AZ中可用的一个NFS,所以需要 VPC ID 应该是比较容易理解的。
下面介绍一下如何通过 aws cli 创建EFS及其Access Points,完整的脚本可以在这里下载 create_efs.sh, create_access_points.sh。
设置参数
这些变量定义了我们当前的aws环境以及要创建的资源名称等信息。
首先我们可以设置一些变量定义当前环境
AWS_PROFILE=myProfile AWS_REGION=us-west-2 CLUSTER_NAME=myCluster
设置中间过程中会用到的常量
MOUNT_TARGET_GROUP_NAME=mySG4EFS MOUNT_TARGET_GROUP_DESC="NFS access to EFS from EKS worker nodes" EFS_NAME=myEfsName
1)获取 VPC ID
因为这里创建出来的EFS要供 EKS 的pod使用,所以VPC的获取是根据eks cluster得到的。
VPC_ID=$(aws eks describe-cluster --profile $AWS_PROFILE --region $AWS_REGION --name $CLUSTER_NAME \ --query "cluster.resourcesVpcConfig.vpcId" --output text) echo "The $CLUSTER_NAME includes the VPC $VPC_ID"
2)获取VPC下的 CIDR
CIDR_BLOCK=$(aws ec2 describe-vpcs --profile $AWS_PROFILE --region $AWS_REGION \ --vpc-ids $VPC_ID --query "Vpcs[].CidrBlock" --output text) echo "The CIDR blocks in the $VPC_ID : $CIDR_BLOCK"
3)在VPC上创建Security Group
MOUNT_TARGET_GROUP_ID=$(aws ec2 create-security-group --profile $AWS_PROFILE --region $AWS_REGION \ --group-name $MOUNT_TARGET_GROUP_NAME \ --description "$MOUNT_TARGET_GROUP_DESC" \ --vpc-id $VPC_ID \ | jq --raw-output '.GroupId')
4)设置去安全组的ingres对2049端口开放
aws ec2 authorize-security-group-ingress --profile $AWS_PROFILE --region $AWS_REGION \ --group-id $MOUNT_TARGET_GROUP_ID --protocol tcp --port 2049 --cidr $CIDR_BLOCK
5)创建 EFS
FILE_SYSTEM_ID=$(aws efs create-file-system --profile $AWS_PROFILE --region $AWS_REGION \ --performance-mode generalPurpose --throughput-mode bursting \ --tags Key=Name,Value=$EFS_NAME \ --backup --encrypted --creation-token "$EFS_NAME"_0 | jq --raw-output '.FileSystemId') echo "The EFS $FILE_SYSTEM_ID is created."
查看某个efs:
aws efs describe-file-systems --file-system-id $FILE_SYSTEM_ID
EFS资源已经创建出来了,要让它能被使用就需要把它mount到VPC的 public subnets 上。
一个subnet是public的还是private的,并不是通过subnet对象的某个属性标识的,而是要看路由表里这个subnet有没有通向0.0.0.0的internet gateway。下面的几个步骤就用于找到 public subnet 并把EFS mount到这些 public subnets。
6) 得到 eks 里的 subnetIds
eksSubnetIds=($(aws eks describe-cluster --profile $AWS_PROFILE --region $AWS_REGION \ --name $CLUSTER_NAME --query "cluster.resourcesVpcConfig.subnetIds" \ --output text)) echo "The eks cluster $CLUSTER_NAME VPC $VPC_ID includes the subnets: $eksSubnetIds"
7) 找到 internet gateway
IGW_ID=$(aws ec2 describe-internet-gateways --profile $AWS_PROFILE --region $AWS_REGION \ --filters Name=attachment.vpc-id,Values=${VPC_ID} \ --query "InternetGateways[].InternetGatewayId" \ | jq -r '.[0]') echo "The internet gateway in the VPC $VPC_ID is $IGW_ID" if [ "null" = "$IGW_ID" ] ; then echo "Can't find public IGW in VPN, exit ..."
8) 找到 public subnets
for subnetId in ${eksSubnetIds[@]} echo "Check the subnet " $subnetId IGW_IN_ROUTS=$(aws ec2 describe-route-tables --profile $AWS_PROFILE --region $AWS_REGION \ --filter Name=association.subnet-id,Values=$subnetId \ --query "RouteTables[].Routes[]" \ | jq -r '.[] | select(.DestinationCidrBlock=="0.0.0.0/0") | .GatewayId') if [ -z $IGW_IN_ROUTS -o "null" = $IGW_IN_ROUTS ] ; then echo "The subnet $subnetId is a private subnet." echo "The subnet $subnetId is a public subnet. $IGW_ID $IGW_IN_ROUTS" if [ "$IGW_ID" = "$IGW_IN_ROUTS" ] ; then echo "Creating the mount target in the subnet $subnetId." aws efs create-mount-target --profile $AWS_PROFILE --region $AWS_REGION \ --file-system-id $FILE_SYSTEM_ID \ --subnet-id $subnetId \ --security-groups $MOUNT_TARGET_GROUP_ID elif [ "null" != "$IGW_IN_ROUTS" ] ; then echo "WARNING: The IGW id in routes does not equal with the one in VPC!"
10) 创建 Access Point
到这里这个NFS已经可以在这个VPC里提供服务了。如果你的目录需要更精细的用户、组的设置,可以通过下面的命令创建 Access Point 来做更精细的控制。
ACCESS_POING_NAME=myAP FILE_SYSTEM_ID=fs-082697b352a3230d1 AP_USER='{"Uid": 123, "Gid": 123, "SecondaryGids": [20]}' AP_ROOT_DIR='/myapp/logs,CreationInfo={OwnerUid=123,OwnerGid=123,Permissions=0755}' aws efs create-access-point --profile $AWS_PROFILE --region $AWS_REGION \ --tags Key=name,Value=$ACCESS_POING_NAME \ --client-token "$ACCESS_POING_NAME" \ --file-system-id $FILE_SYSTEM_ID \ --posix-user $AP_USER \ --root-directory Path=$AP_ROOT_DIR
下面显示的是在eks中通过StorageClass自动分配EFS资源的场景下,如何如何设置相关属性。 参考 https://github.com/kubernetes-sigs/aws-efs-csi-driver 查看完整的parameter列表。
kind: StorageClass apiVersion: storage.k8s.io/v1 metadata: name: efs-sc provisioner: efs.csi.aws.com mountOptions: - tls - iam parameters: provisioningMode: efs-ap fileSystemId: fs-012345678901010 directoryPerms: "700" gidRangeStart: "1000" gidRangeEnd: "2000" basePath: "/dynamic_provisioning"
Reference:
[1]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/file-storage.html
[2]: https://docs.aws.amazon.com/efs/latest/ug/creating-using.html
[3]: https://docs.aws.amazon.com/cli/latest/reference/efs/create-file-system.html
[4]: https://docs.aws.amazon.com/cli/latest/reference/efs/create-access-point.html
[5]: https://docs.aws.amazon.com/efs/latest/ug/creating-using-create-fs.html#creating-using-fs-part1-cli
[6]: https://stackoverflow.com/questions/48830793/aws-vpc-identify-private-and-public-subnet
[7]: https://www.baeldung.com/linux/jq-command-json
[8]: https://aws.amazon.com/premiumsupport/knowledge-center/eks-troubleshoot-efs-volume-mount-issues/
[9]: https://github.com/kubernetes-sigs/aws-efs-csi-driver- Use Amazon S3 with Amazon EC2
下面的这张概念图很好的描述各种存储的位置层次。
在EKS的node如果是基于EC2的,那么PV就可以利用以上除去S3之外的其余三种作为底层存储。
参考:https://docs.aws.amazon.com/eks/latest/userguide/storage.html关于EC2可以使用的存储的特性、使用场景,推荐阅读官方文档:https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Storage.html。
使用 aws cli 创建一个EFS资源
可以在AWS Console中创建EFS https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AmazonEFS.html。
也可以通过使用aws cli在命令行创建,https://www.eksworkshop.com/beginner/190_efs/launching-efs/。
下面是根据上文得到的,通过aws cli创建EFS的脚本。
假设,你的aws profile是myAwsProfile、eks所在region是us-west-2、eks集群名称是myCluster,而要创建的EFS名称是 my-test-efs
为了使用EFS需要创建SecurityGroup来允许对NFS端口2049的使用,这里设置SG名称为 SG_efs_demo。设置环境变量:
# Set the input env vars export AWS_PROFILE=myAwsProfile export AWS_REGION=us-west-2 export CLUSTER_NAME=myCluster
# Set the output env vars export MOUNT_TARGET_GROUP_NAME=perf-test-efs-group export MOUNT_TARGET_GROUP_DESC="NFS access to EFS from EKS worker nodes" export EFS_NAME=my-test-efs
获取 VPC ID
# Get eks cluster's VPC ID. export VPC_ID=$(aws eks describe-cluster --profile $AWS_PROFILE --region $AWS_REGION --name $CLUSTER_NAME --query "cluster.resourcesVpcConfig.vpcId" --output text) echo $VPC_ID
获取 VPC 里的 subnets
# Get the subnets's CIDR in the VPC. export CIDR_BLOCK=$(aws ec2 describe-vpcs --profile $AWS_PROFILE --region $AWS_REGION --vpc-ids $VPC_ID --query "Vpcs[].CidrBlock" --output text) echo $CIDR_BLOCK
创建 Security Group
# Create SG(allow port 2049 in ingress for all of the CIDR in VPC) for EFS export MOUNT_TARGET_GROUP_ID=$(aws ec2 create-security-group --profile $AWS_PROFILE --region $AWS_REGION --group-name $MOUNT_TARGET_GROUP_NAME --description "$MOUNT_TARGET_GROUP_DESC" --vpc-id $VPC_ID | jq --raw-output '.GroupId') aws ec2 authorize-security-group-ingress --profile $AWS_PROFILE --region $AWS_REGION --group-id $MOUNT_TARGET_GROUP_ID --protocol tcp --port 2049 --cidr $CIDR_BLOCK # Get back the security-group informaation. aws ec2 describe-security-groups --filters Name=group-name,Values=$MOUNT_TARGET_GROUP_NAME
创建 EFS
# Create EFS. https://docs.aws.amazon.com/cli/latest/reference/efs/create-file-system.html # https://docs.aws.amazon.com/efs/latest/ug/creating-using-create-fs.html#creating-using-fs-part1-cli # If no creation-token is provied, you'd better go to the AWS EFS Console # to make sure the EFS is created or not to avoid too many EFS reources are created. # 使用efs name作为前缀加序号的方式做 creation-token 是个不错的选择, export FILE_SYSTEM_ID=$(aws efs create-file-system --profile $AWS_PROFILE --region $AWS_REGION \ --performance-mode generalPurpose --throughput-mode bursting --tags Key=name,Value=$EFS_NAME \ --backup --encrypted --creation-token "$EFS_NAME"_0 | jq --raw-output '.FileSystemId')
找到 VPC 中的 pubilc subnets
# Find out the public subtnets from the subnets of the eks cluster. # 得到eks VPC的所有 subnetIds # export eksSubnetIDs=($(aws eks describe-cluster --profile $AWS_PROFILE --region $AWS_REGION --name $CLUSTER_NAME --query "cluster.resourcesVpcConfig.subnetIds" --output text)) # 找到 Internet GW export IGW_ID=`aws ec2 describe-internet-gateways \ --filters Name=attachment.vpc-id,Values=${VPC_ID} \ --query "InternetGateways[].InternetGatewayId" \ | jq -r '.[0]'` # 找到 public subnets: https://stackoverflow.com/questions/48830793/aws-vpc-identify-private-and-public-subnet export PUBLIC_SUBNETS=`aws ec2 describe-route-tables \ --query 'RouteTables[*].Associations[].SubnetId' \ --filters "Name=vpc-id,Values=${VPC_ID}" \ "Name=route.gateway-id,Values=${IGW_ID}" \ | jq . -c`
把 EFS mount 到 public subnets,这样EKS里的worker nodes就都可以访问这些EFS了。
for subnet in ${PUBLIC_SUBNETS[@]} echo "creating mount target in " $subnet aws efs create-mount-target --profile $AWS_PROFILE --region us-west-2 --file-system-id $FILE_SYSTEM_ID --subnet-id $subnet --security-groups $MOUNT_TARGET_GROUP_ID
创建 Access Points
有了EFS之后就可以创建 AccessPoints 供应用程序使用了。关于更详细的Access Points介绍:https://docs.aws.amazon.com/efs/latest/ug/efs-access-points.html
ACCESS_POING_NAME=ap_db2 FILE_SYSTEM_ID=fs-055b5f1fcc7df3e4b AP_DIR_USER='{"Uid": 70, "Gid": 70, "SecondaryGids": [70]}' AP_ROOT_DIR='/mydataPath,CreationInfo={OwnerUid=70,OwnerGid=70,Permissions=0755}' aws efs create-access-point --profile $AWS_PROFILE --region $AWS_REGION \ --tags Key=name,Value=$ACCESS_POING_NAME \ --client-token "$ACCESS_POING_NAME"_2 \ --file-system-id $FILE_SYSTEM_ID \ --posix-user $AP_DIR_USER \ --root-directory Path=$AP_ROOT_DIR
1)Path的格式必须满足下面的正则
^(\/|(\/(?!\.)+[^$#<>;`|&?{}^*/\n]+){1,4})$
所以path不能以
/
结尾。
2)创建的access point 可以同名、可以同名同path,唯一标识access point的是
Access point ID
3) 根据id删除一个 access point