一款基于.NET Core的认证授权解决方案-葫芦藤1.0开源啦 - 福禄网络技术团队 - 博客园

mikel阅读(852)

来源: 一款基于.NET Core的认证授权解决方案-葫芦藤1.0开源啦 – 福禄网络技术团队 – 博客园

背景

18年公司准备在技术上进行转型,而公司技术团队是互相独立的,新技术的推动阻力很大。我们需要找到一个切入点。公司的项目很多,而各个系统之间又不互通,导致每套系统都有一套登录体系,给员工和客户都带来极大的不便。那么从登录切入进去无疑最合适,对于各个团队的技术改造成本也不大。所以我们团队第一个项目就是搭建一套统一登录认证授权系统,那么葫芦藤项目应运而生。

技术方案

后端框架:.NET Core3.1(后期会推出 .NET 5版本)

前端框架:React

数据库:mySQL(可根据实际情况,自由切换)

中间件:redis

详细功能

认证授权服务

基于IdentityServer4实现的协议,支持网站、本地应用、移动端、web服务等应用的认证授权逻辑。

单点登录登出

支持各种类型应用上的单点登录登出。开箱即用的基础用户管理模块,包括:注册、登录、手机验证码、忘记密码等。为了安全考虑,集成了腾讯图形验证码。

第三方登录(微信、钉钉)

完善的第三方登录支持。支持首次登录时绑定已存在用户或注册新用户后,自动绑定。

如何快速使用

1.下载代码

clone代码到本地。根目录结构如下:

20201103153907

其中,backend存放的是后端代码,frontend存放的是前端代码。

进入backend目录,使用Visual Studio打开解决方案。目录结构如下:

20201103154250

2.生成数据库

首先在Fulu.Passport.Web中找到appsettings.Development.json文件。编辑数据库连接字符串:

20201103155350

打开程序包管理器,切换默认项目为:Fulu.Passport.Web, 如下图所示:

20201106111334

然后在程序包管理器中执行如下命令:


Add-Migration Init

最后执行完成后,再执行如下命令:

update-database

执行完以上操作后,如没有报错,则会创建数据库,并会在Client表中创建一条测试数据,如下图所示:

20201103160408

3.按F5启动后端服务

注:由于项目中依赖redis来处理缓存,所以正式启动之前,需要将appsettings.Development.json文件里的redis配置改为你自己的。

4.启动前端

切换目录到frontend,在命令行中执行如下命令:

npm install

执行完毕后,执行如下命令:

npm run demo

执行结果如下图所示:

20201103161300

然后通过http://localhost:8080进行访问。界面如下所示:

20201103174200

至此,前后端服务已启动完毕,一个开箱即用的认证授权服务就完成了。

5.新客户端如何快速接入认证服务?

认证授权服务存在的意义就是提供统一的认证授权入口,有了这个服务后,每个新的客户端应用无需单独开发认证授权模块。下面就来一起看下如何快速将新应用接入到认证授权服务。(此处以 ASP.NET Core作为示例,其他语言大同小异)。

示例代码在sample文件夹中,如下图所示:

20201104165955

在正式接入之前,必须先申请应用。(此版本未提供应用管理服务)通过在数据库中添加示例信息,如下图所示:

20201104192124

示例SQL脚本:

INSERT INTO `fulusso`.`client`(`client_secret`, `full_name`, `host_url`, `redirect_uri`, `description`, `enabled`, `id`) VALUES ('14p9ao1gxu4q3sp8ogk8bq4gkct59t9w', '葫芦藤2', 'http://localhost:5003/', 'http://localhost:5003', NULL, 1, UUID());

其中,redirect_uri参数指的是从认证服务获取code之后,重定向的url。为了开发的方便,我们的认证服务中仅校验回调域名的域名,不会校验完整的地址。比如,你的redirect_uri为http://www.xxx.com/abc/aaa,则数据库中的redirect_uri字段填写http://www.xxx.com即可。

应用信息导入到数据库后,在Startup类的ConfigureServices方法中,添加如下代码:

services.AddServiceAuthorize(o =>
{
    o.AllowClientToken = true;
    o.AllowUserToken = true;
    o.OnClientValidate = false;
    o.Authority = "http://localhost:5000";
    o.ValidateAudience = false;
    o.ClientId = Configuration["AppSettings:ClientId"];
    o.ClientSecret = Configuration["AppSettings:ClientSecret"];
});

注:需添加Fulu.Service.Authorize项目引用,如下图所示:

20201104170401

然后在Configure方法中,添加如下代码:

 app.UseRouting();
 app.UseJwtAuthorize();
 app.UseAuthorization();

其中,UseJwtAuthorize是自定义的中间件,为了实现OAuth2.0的授权码的逻辑。
限于篇幅,具体代码不在此列出。可在代码仓库中查看。

到此为止,这个新应用就成功的接入到认证服务了。

当未登录的时候,访问此应用的页面会自动跳转到认证服务的login界面。登录之后,会重定向回登录之前的页面。如下图所示:

aa

下一版功能规划

1.更多的第三方平台的接入(QQ、微博等)

2.api授权服务

3.更安全的二次验证,集成google令牌

4.应用管理

等等~~~~,尽请期待。

体验

演示地址:https://account.suuyuu.cn/

代码仓库:https://github.com/fuluteam/fulusso

(3)ASP.NET Core3.1 Ocelot认证 - 暗断肠 - 博客园

mikel阅读(756)

来源: (3)ASP.NET Core3.1 Ocelot认证 – 暗断肠 – 博客园

.认证

当客户端通过Ocelot访问下游服务的时候,为了保护下游资源服务器会进行认证鉴权,这时候需要在Ocelot添加认证服务。添加认证服务后,随后使用Ocelot基于声明的任何功能,例如授权或使用Token中的值修改请求。用户必须像往常一样在其Startup.cs中注册身份验证服务,但是他们为每次注册提供一个方案(身份验证提供者密钥),例如:

复制代码
public void ConfigureServices(IServiceCollection services)
{
    var authenticationProviderKey = "TestKey";
    services.AddAuthentication()
        .AddJwtBearer(authenticationProviderKey, x =>
        {
        });
}
复制代码

在此Ocelot认证项目示例中,TestKey是已注册此提供程序的方案。然后我们将其映射到配置中的Routes路由,例如:

复制代码
{
    "Routes": [
        {
            "DownstreamPathTemplate": "/api/customers",
            "DownstreamScheme": "http",
            "DownstreamHost": "localhost",
            "DownstreamPort": 9001,
            "UpstreamPathTemplate": "/customers",
            "UpstreamHttpMethod": [ "Get" ],
            "AuthenticationOptions": {
                "AuthenticationProviderKey": "TestKey",
                "AllowedScopes": []
            }
        }
    ]
}
复制代码

Ocelot运行时,它将查看Routes.AuthenticationOptions.AuthenticationProviderKey并检查是否存在使用给定密钥注册的身份验证提供程序。如果不存在,则Ocelot将不会启动,如果存在,则Routes将在执行时使用该提供程序。
如果对路由进行身份验证,Ocelot将在执行身份验证中间件时调用与之关联的任何方案。如果请求通过身份验证失败,Ocelot将返回http状态代码401。

2.JWT Tokens Bearer认证

Json Web Token (JWT),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519)。该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

2.1JWT令牌结构

在紧凑的形式中,JSON Web Tokens由dot(.)分隔的三个部分组成,它们是:
Header头、Payload有效载荷、Signature签名
因此,JWT通常如下所示:xxxxx.yyyyy.zzzzz(Header.Payload.Signature)

2.1.1Header头

标头通常由两部分组成:令牌的类型,即JWT,以及正在使用的签名算法,例如HMAC SHA256或RSA。例如:

{
  "alg": "HS256",
  "typ": "JWT"
}

然后,这个JSON被编码为Base64Url,形成JWT的第一部分。

2.1.2Payload有效载荷

Payload部分也是一个JSON对象,用来存放实际需要传递的数据。JWT规定了7个官方字段,供选用。
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。例如:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

注意,JWT默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。这个JSON对象也要使用Base64URL算法转成字符串。

2.1.3.Signature签名

Signature部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用Header里面指定的签名算法(默认是HMAC SHA256),按照下面的公式产生签名。

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

签名用于验证消息在此过程中未被更改,并且,在使用私钥签名的令牌的情况下,它还可以验证JWT的发件人是否是它所声称的人。
把他们三个全部放在一起,输出是三个由点分隔的Base64-URL字符串,可以在HTML和HTTP环境中轻松传递,而与基于XML的标准(如SAML)相比更加紧凑。
下面显示了一个JWT,它具有先前的头和有效负载编码,并使用机密签名。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoid3prNzAzIiwibmJmIjoiMTU5MjE0MzkzNyIsImV4cCI6MTU5MjE0Mzk5OCwiaXNzIjoiYXV0aC5qd3QuY2MiLCJhdWQiOiJkZW5nd3V8MjAyMC82LzE0IDIyOjEyOjE5In0
.4RiwhRy0rQkZjclOFWyTpmW7v0AMaL3aeve1L-eWIz0

其实一般发送用户名和密码获取token那是由Identity4来完成的,包括验证用户,生成JwtToken。但是项目这里是由System.IdentityModel.Tokens类库来生成JwtToken。最后返回jwt令牌token给用户。JwtToken解码可以通过https://jwt.io/中进行查看。

3.项目演示

3.1APIGateway项目

在该项目中启用身份认证来保护下游api服务,使用JwtBearer认证,将默认的身份验证方案设置为TestKey。在appsettings.json文件中配置认证中密钥(Secret)跟受众(Aud)信息:

复制代码
{
    "Audience": {
        "Secret": "Y2F0Y2hlciUyMHdvbmclMjBsb3ZlJTIwLm5ldA==",
        "Iss": "http://www.c-sharpcorner.com/members/catcher-wong",
        "Aud": "Catcher Wong"
    }
}
复制代码

Startup添加身份认证代码如下:

复制代码
public void ConfigureServices(IServiceCollection services)
{
    //获取appsettings.json文件中配置认证中密钥(Secret)跟受众(Aud)信息
    var audienceConfig = Configuration.GetSection("Audience");
    //获取安全秘钥
    var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(audienceConfig["Secret"]));
    //token要验证的参数集合
    var tokenValidationParameters = new TokenValidationParameters
    {
        //必须验证安全秘钥
        ValidateIssuerSigningKey = true,
        //赋值安全秘钥
        IssuerSigningKey = signingKey,
        //必须验证签发人
        ValidateIssuer = true,
        //赋值签发人
        ValidIssuer = audienceConfig["Iss"],
        //必须验证受众
        ValidateAudience = true,
        //赋值受众
        ValidAudience = audienceConfig["Aud"],
        //是否验证Token有效期,使用当前时间与Token的Claims中的NotBefore和Expires对比
        ValidateLifetime = true,
        //允许的服务器时间偏移量
        ClockSkew = TimeSpan.Zero,
        //是否要求Token的Claims中必须包含Expires
        RequireExpirationTime = true,
    };
    //添加服务验证,方案为TestKey
    services.AddAuthentication(o =>
    {
        o.DefaultAuthenticateScheme = "TestKey";
    })
    .AddJwtBearer("TestKey", x =>
        {
            x.RequireHttpsMetadata = false;
            //在JwtBearerOptions配置中,IssuerSigningKey(签名秘钥)、ValidIssuer(Token颁发机构)、ValidAudience(颁发给谁)三个参数是必须的。
            x.TokenValidationParameters = tokenValidationParameters;
        });
    //添加Ocelot网关服务时,包括Secret秘钥、Iss签发人、Aud受众
    services.AddOcelot(Configuration);
}
public async void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    //使用认证服务
    app.UseAuthentication();
    //使用Ocelot中间件
    await app.UseOcelot();
}
复制代码
3.1.1Identity Server承载JWT Token

在第二小节介绍JWT Token认证时候,我们都知道一般发送用户名和密码获取Token那是由Identity4来完成的,包括验证用户,生成JWT Token。也就是说Identity Server承载了JWT Token认证功能。为了使用IdentityServer承载Token,请像往常一样在ConfigureServices中使用方案(密钥)注册IdentityServer服务。如果您不知道如何执行此操作,请查阅IdentityServer文档。

复制代码
public void ConfigureServices(IServiceCollection services)
{
    var authenticationProviderKey = "TestKey";
    Action<IdentityServerAuthenticationOptions> options = o =>
        {
            o.Authority = "https://whereyouridentityserverlives.com";
            o.ApiName = "api";
            o.SupportedTokens = SupportedTokens.Both;
            o.ApiSecret = "secret";
        };
    services.AddAuthentication()
        .AddIdentityServerAuthentication(authenticationProviderKey, options);
    services.AddOcelot();
}
复制代码

在Identity4中是由Authority参数指定OIDC服务地址,OIDC可以自动发现Issuer, IssuerSigningKey等配置,而o.Audience与x.TokenValidationParameters = new TokenValidationParameters { ValidAudience = “api” }是等效的。

3.2AuthServer项目

此服务主要用于客户端请求受保护的资源服务器时,认证后产生客户端需要的JWT Token,生成JWT Token关键代码如下:

复制代码
[Route("api/[controller]")]
public class AuthController : Controller
{
    private IOptions<Audience> _settings;
    public AuthController(IOptions<Audience> settings)
    {
        this._settings = settings;
    }
    /// <summary>
    ///用户使用 用户名密码 来请求服务器
    ///服务器进行验证用户的信息
    ///服务器通过验证发送给用户一个token
    ///客户端存储token,并在每次请求时附送上这个token值, headers: {'Authorization': 'Bearer ' + token}
    ///服务端验证token值,并返回数据
    /// </summary>
    /// <param name="name"></param>
    /// <param name="pwd"></param>
    /// <returns></returns>
    [HttpGet]
    public IActionResult Get(string name, string pwd)
    {
        //验证登录用户名和密码
        if (name == "catcher" && pwd == "123")
        {
            var now = DateTime.UtcNow;
            //添加用户的信息,转成一组声明,还可以写入更多用户信息声明
            var claims = new Claim[]
            {
                //声明主题
                new Claim(JwtRegisteredClaimNames.Sub, name),
                    //JWT ID 唯一标识符
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                    //发布时间戳 issued timestamp
                new Claim(JwtRegisteredClaimNames.Iat, now.ToUniversalTime().ToString(), ClaimValueTypes.Integer64)
            };
            //下面使用 Microsoft.IdentityModel.Tokens帮助库下的类来创建JwtToken

            //安全秘钥
            var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_settings.Value.Secret));

            //声明jwt验证参数
            var tokenValidationParameters = new TokenValidationParameters
            {
                //必须验证安全秘钥
                ValidateIssuerSigningKey = true,
                //赋值安全秘钥
                IssuerSigningKey = signingKey,
                //必须验证签发人
                ValidateIssuer = true,
                //赋值签发人
                ValidIssuer = _settings.Value.Iss,
                //必须验证受众
                ValidateAudience = true,
                //赋值受众
                ValidAudience = _settings.Value.Aud,
                //是否验证Token有效期,使用当前时间与Token的Claims中的NotBefore和Expires对比
                ValidateLifetime = true,
                //允许的服务器时间偏移量
                ClockSkew = TimeSpan.Zero,
                //是否要求Token的Claims中必须包含Expires
                RequireExpirationTime = true,
            };
            var jwt = new JwtSecurityToken(
                //jwt签发人
                issuer: _settings.Value.Iss,
                //jwt受众
                audience: _settings.Value.Aud,
                //jwt一组声明
                claims: claims,
                notBefore: now,
                //jwt令牌过期时间
                expires: now.Add(TimeSpan.FromMinutes(2)),
                //签名凭证: 安全密钥、签名算法
                signingCredentials: new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256)
            );
            //生成jwt令牌(json web token)
            var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
            var responseJson = new
            {
                access_token = encodedJwt,
                expires_in = (int)TimeSpan.FromMinutes(2).TotalSeconds
            };
            return Json(responseJson);
        }
        else
        {
            return Json("");
        }
    }
}
public class Audience
{
    public string Secret { get; set; }
    public string Iss { get; set; }
    public string Aud { get; set; }
}
复制代码

appsettings.json文件中配置认证中密钥(Secret)跟受众(Aud)信息:

复制代码
{
    "Audience": {
        "Secret": "Y2F0Y2hlciUyMHdvbmclMjBsb3ZlJTIwLm5ldA==",
        "Iss": "http://www.c-sharpcorner.com/members/catcher-wong",
        "Aud": "Catcher Wong"
    }
}
复制代码

3.3CustomerAPIServices项目

该项目跟APIGateway项目是一样的,为了保护下游api服务,使用JwtBearer认证,将默认的身份验证方案设置为TestKey。在appsettings.json文件中配置认证中密钥(Secret)跟受众(Aud)信息:

复制代码
{
    "Audience": {
        "Secret": "Y2F0Y2hlciUyMHdvbmclMjBsb3ZlJTIwLm5ldA==",
        "Iss": "http://www.c-sharpcorner.com/members/catcher-wong",
        "Aud": "Catcher Wong"
    }
}
复制代码

Startup添加身份认证代码如下:

复制代码
public void ConfigureServices(IServiceCollection services)
{
    //获取appsettings.json文件中配置认证中密钥(Secret)跟受众(Aud)信息
    var audienceConfig = Configuration.GetSection("Audience");
    //获取安全秘钥
    var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(audienceConfig["Secret"]));
    //token要验证的参数集合
    var tokenValidationParameters = new TokenValidationParameters
    {
        //必须验证安全秘钥
        ValidateIssuerSigningKey = true,
        //赋值安全秘钥
        IssuerSigningKey = signingKey,
        //必须验证签发人
        ValidateIssuer = true,
        //赋值签发人
        ValidIssuer = audienceConfig["Iss"],
        //必须验证受众
        ValidateAudience = true,
        //赋值受众
        ValidAudience = audienceConfig["Aud"],
        //是否验证Token有效期,使用当前时间与Token的Claims中的NotBefore和Expires对比
        ValidateLifetime = true,
        //允许的服务器时间偏移量
        ClockSkew = TimeSpan.Zero,
        //是否要求Token的Claims中必须包含Expires
        RequireExpirationTime = true,
    };
    //添加服务验证,方案为TestKey
    services.AddAuthentication(o =>
    {
        o.DefaultAuthenticateScheme = "TestKey";
    })
    .AddJwtBearer("TestKey", x =>
        {
            x.RequireHttpsMetadata = false;
            //在JwtBearerOptions配置中,IssuerSigningKey(签名秘钥)、ValidIssuer(Token颁发机构)、ValidAudience(颁发给谁)三个参数是必须的。
            x.TokenValidationParameters = tokenValidationParameters;
        });

    services.AddMvc();
}
public void Configure(IApplicationBuilder app)
{
    //使用认证服务
    app.UseAuthentication();
    app.UseMvc();
}
复制代码

在CustomersController下添加一个需要认证方法,一个不需要认证方法:

复制代码
[Route("api/[controller]")]
public class CustomersController : Controller
{
    //添加认证属性
    [Authorize]
    [HttpGet]
    public IEnumerable<string> Get()
    {
        return new string[] { "Catcher Wong", "James Li" };
    }
    [HttpGet("{id}")]
    public string Get(int id)
    {
        return $"Catcher Wong - {id}";
    }
}
复制代码

3.4ClientApp项目

该项目是用来模拟客户端访问资源服务器整个认证流程测试项目,在Program主程序可以看到如下代码:

复制代码
class Program
{
    static void Main(string[] args)
    {
        HttpClient client = new HttpClient();

        client.DefaultRequestHeaders.Clear();
        client.BaseAddress = new Uri("http://localhost:9000");

        // 1. without access_token will not access the service
        //    and return 401 .
        var resWithoutToken = client.GetAsync("/customers").Result;

        Console.WriteLine($"Sending Request to /customers , without token.");
        Console.WriteLine($"Result : {resWithoutToken.StatusCode}");

        //2. with access_token will access the service
        //   and return result.
        client.DefaultRequestHeaders.Clear();
        Console.WriteLine("\nBegin Auth....");
        var jwt = GetJwt();
        Console.WriteLine("End Auth....");
        Console.WriteLine($"\nToken={jwt}");

        client.DefaultRequestHeaders.Add("Authorization", $"Bearer {jwt}");
        var resWithToken = client.GetAsync("/customers").Result;

        Console.WriteLine($"\nSend Request to /customers , with token.");
        Console.WriteLine($"Result : {resWithToken.StatusCode}");
        Console.WriteLine(resWithToken.Content.ReadAsStringAsync().Result);

        //3. visit no auth service
        Console.WriteLine("\nNo Auth Service Here ");
        client.DefaultRequestHeaders.Clear();
        var res = client.GetAsync("/customers/1").Result;

        Console.WriteLine($"Send Request to /customers/1");
        Console.WriteLine($"Result : {res.StatusCode}");
        Console.WriteLine(res.Content.ReadAsStringAsync().Result);

        Console.Read();
    }
    private static string GetJwt()
    {
        HttpClient client = new HttpClient();

        client.BaseAddress = new Uri( "http://localhost:9000");
        client.DefaultRequestHeaders.Clear();

        var res2 = client.GetAsync("/api/auth?name=catcher&pwd=123").Result;

        dynamic jwt = JsonConvert.DeserializeObject(res2.Content.ReadAsStringAsync().Result);

        return jwt.access_token;
    }
}
复制代码

运行项目看看测试结果:

结合代码,我们能看到当客户端通过Ocelot网关访问下游服务http://localhost:9000/api/Customers/Get方法时候,因为该方法是需要通过认证才返回处理结果的,所以会进行JWT Token认证,如果发现没有Token,Ocelot则返回http状态代码401拒绝访问。如果我们通过GetJwt方法在AuthServer服务上登录认证获取到授权Token,然后再访问该资源服务器接口,立即就会返回处理结果,通过跟而未加认证属性的http://localhost:9000/api/Customers/Get/{id}方法对比,我们就知道,Ocelot认证已经成功了!

4.总结

该章节只是结合demo项目简单介绍在Ocelot中如何使用JWT Token认证。其实正式环境中,Ocelot是应该集成IdentityServer认证授权的,同样的通过重写Ocelot中间件我们还可以把configuration.json的配置信息存储到数据库或者缓存到Redis中。

参考文献:
Ocelot官网

增势税票识别助手,可识别照片、扫描件、电子票、形成电子台帐。 - 一壶茶水 - 博客园

mikel阅读(1184)

来源: 增势税票识别助手,可识别照片、扫描件、电子票、形成电子台帐。 – 一壶茶水 – 博客园

 

该助手可以快速、准确的识别并读取发票相关字段信息,与传统的人工录入核对的方式相比,时效性更高,数据准确性更强。

同时,软件具备批量导出功能,数据读取字段与公司综项系统相一致,可以实现快捷的导入到综项系统中,大大减少操作人员的工作量。

首先本小工具使用C# winfrom 实现,其中主要是使用了百度智能云OCR文字识别技术,调用期官网接口,很简单,搭配NPOI Execl操作类库,

利用Spire.pdf类库,把pdf格式发票,转换为png图片格式。自动识别图片、pdf格式发票,发票可以用高拍仪、手机拍照、扫面件等都可以识别。

  其他说明:本程序借助百度智能云API作为基础的发票识别技术,识别准确率在98%以上,同时,由于百度智能云API每天免费授权识别限制在500次/天且可以对个人用户申请开放。

在授权管理中个人可以根据使用量的多少自行申请百度智能云API认证授权API key和Secret key,然后在发票识别助手中认证。

发票识别助手共分5个功能模块,操作相对很简单,第一步点击添加发票按钮,选择要识别的发票信息。注意说明:目前图片格式支持jpg、png、bmp,图片的长和宽要求最短边大于10px,

最长边小于2048px;图像编码后大小必须小于4M,建议不要超过1M;第二步点击识别发票按钮,系统开始识别发票信息,识别完成后,发票信息会自动生成;

 

介绍一下关键的代码:

一、获取百度云API token,这个是官方给的,直接拿过来用就可以了。

二、增值税票识别请求过程和参数传递,也是官方给的例子,自己按照需求修改一下就可以了。

 三、这里的部分是把pdf格式的发票,自动转换为png格式,提供出百度云api需要的文件格式。

 四、获取api返回的数据,输出到dataGridView中。

五、导出发票明细到EXECL表格中。

操作说明如下:

事务(进程 ID 51)与另一个进程被死锁在 锁 资源上,并且已被选作死锁牺牲品_小白博客-CSDN博客

mikel阅读(1458)

来源: 事务(进程 ID 51)与另一个进程被死锁在 锁 资源上,并且已被选作死锁牺牲品_小白博客-CSDN博客

所有死锁的原因可归结为资源的竞争
表现一:

一个用户A 访问表A(锁住了表A),然后又访问表B 另一个用户B 访问表B(锁住了表B),然后企图访问表A 这时用户A由于用户B已经锁住表B,它必须等待用户B释放表B,才能继续,好了他老人家就只好老老实实在这等了 同样用户B要等用户A释放表A才能继续这就死锁了

解决方法:
    这种死锁是由于你的程序的'bug'产生的,除了调整你的程序的逻辑别无他法,仔细分析你程序的逻辑,
    1:尽量避免同时锁定两个资源
    2: 必须同时锁定两个资源时,要保证在任何时刻都应该按照相同的顺序来锁定资源.
  • 1
  • 2
  • 3
  • 4
表现二:

用户A读一条纪录,然后修改该条纪录,这是用户B修改该条纪录这里用户A的事务里锁的性质由共享锁企图上升到独占锁(forupdate),而用户B里的独占锁由于A有共享锁存在所以必须等A释放掉共享锁,而A由于B的独占锁而无法上升的独占锁也就不可能释放共享锁,于是出现了死锁

解决方法:
    让用户A的事务(即先读后写类型的操作),在select 时就是用Update lock
    语法如下:
    select * from table1 (updlock) where ....
  • 1
  • 2
  • 3
  • 4

原文地址


NOLOCK

NOLOCK在概念上类似于READ UNCOMMITTED隔离级别,并且只针对于SELECT查询语句,它不会获取表的共享锁,换句话说不会阻止排它锁来更新数据行。当我们对表进行NOLOCK有什么好处呢?它能够提高并发性能,因为此时SQL Server数据库引擎不必去维护共享锁,由于不会对正在读取的表获取共享锁,所以可能导致未提交的事务也会被读取,所以此时缺点显而易见将导致脏读

SELECT COUNT(*) FROM Example WITH(NOLOCK)
  • 1
READPAST

当在表中用READPAST指定提示时此时SQL Server数据库引擎在返回结果集时将不会返回锁定的行或者数据页。它除了和NOLOCK一样不会导致查询阻塞外,因为不会返回锁定的行记录所以其优点好包括不存在脏读。但是其缺点则是因为不包含锁定的行记录但是很难保证结果集或者修改语句是否包含我们所必须需要返回的行。有可能在我们的业务逻辑中,需要返回我们必须需要的行。它的使用方式和NOLOCK一样

SELECT COUNT(*)FROM Example WITH(READPAST)
  • 1
UPDLOCK

UPDLOCK只是针对于表中的某一行记录来锁定从而阻止其他操作对该行的数据更新,说到这里想必我们已经明了,UPDLOCK是行级别,而排它锁则是表级别,二者不可同日而语。也就说当我们对某一行添加UPDLOCK提示时并不会阻塞其他查询操作

BEGIN TRAN
 select * from Example WITH (UPDLOCK) where SaleID = 1
此时我们再来开一个窗口进行查询,如下:
select * from Example
  • 1
  • 2
  • 3
  • 4
HOLDLOCK

使用HOLDLOCK提示时,此时查询将锁定表且被强制序列化,直到事务完成,才会被释放,其类似于SERIALIZABLE最高隔离级别

BEGIN TRAN
 select * from Example WITH (UPDLOCK,HOLDLOCK) where SaleID = 1
  • 1
  • 2

原文地址

 C#中List集合使用OrderByDescending方法对集合进行倒序排序 -

mikel阅读(2582)

来源: 【转载】 C#中List集合使用OrderByDescending方法对集合进行倒序排序 – 江湖逍遥 – 博客园

C#的List集合操作中,有时候需要针对List集合进行排序操作,如果是对List集合按照元素对象或者元素对象的某个属性进行倒序排序的话,可以使用OrderByDescending方法来实现,OrderByDescending方法属于List集合的扩展方法,方法的调用形式为使用Lambda表达式语句。

(1)对List<int>集合对象list1进行从大到小降序排序可使用下列语句:

List<int> list1 = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
list1 = list1.OrderByDescending(t => t).ToList();

(2)按List集合中的元素对象的某个具体属性进行倒序排序也可使用OrderByDescending方法。

我们需要对List<TestModel>集合对象testList进行排序,排序规则为按照对象属性Index降序排序。

首先看下TestModel的定义:

public class TestModel
{
public int Index { set; get; }

public string Name { set; get; }
}

对testList集合按元素对象的Index属性进行倒序排序可使用下列语句:

List<TestModel> testList = new List<ConsoleApplication1.TestModel>();
testList = testList.OrderByDescending(t => t.Index).ToList();

Web API 强势入门指南 - 微软互联网开发支持 - 博客园

mikel阅读(1014)

来源: Web API 强势入门指南 – 微软互联网开发支持 – 博客园

Web API是一个比较宽泛的概念。这里我们提到Web API特指ASP.NET Web API。

这篇文章中我们主要介绍Web API的主要功能以及与其他同类型框架的对比,最后通过一些相对复杂的实例展示如何通过Web API构建http服务,同时也展示了Visual Studio构建.net项目的各种强大。

目录

什么是 Web API

官方定义如下,强调两个关键点,即可以对接各种客户端(浏览器,移动设备),构建http服务的框架。

ASP.NET Web API is a framework that makes it easy to build HTTP services that reach a broad range of clients, including browsers and mobile devices. ASP.NET Web API is an ideal platform for building RESTful applications on the .NET Framework.

Web API在ASP.NET完整框架中地位如下图,与SignalR一起同为构建Service的框架。Web API负责构建http常规服务,而SingalR主要负责的是构建实时服务,例如股票,聊天室,在线游戏等实时性要求比较高的服务。

Picture20

 

为什么要用 Web API

Web API最重要的是可以构建面向各种客户端的服务。另外与WCF REST Service不同在于,Web API利用Http协议的各个方面来表达服务(例如 URI/request response header/caching/versioning/content format),因此就省掉很多配置。

Picture2

 

当你遇到以下这些情况的时候,就可以考虑使用Web API了。

  • 需要Web Service但是不需要SOAP
  • 需要在已有的WCF服务基础上建立non-soap-based http服务
  • 只想发布一些简单的Http服务,不想使用相对复杂的WCF配置
  • 发布的服务可能会被带宽受限的设备访问
  • 希望使用开源框架,关键时候可以自己调试或者自定义一下框架

功能简介

Web API的主要功能

1. 支持基于Http verb (GET, POST, PUT, DELETE)的CRUD (create, retrieve, update, delete)操作

通过不同的http动作表达不同的含义,这样就不需要暴露多个API来支持这些基本操作。

2. 请求的回复通过Http Status Code表达不同含义,并且客户端可以通过Accept header来与服务器协商格式,例如你希望服务器返回JSON格式还是XML格式。

3. 请求的回复格式支持 JSON,XML,并且可以扩展添加其他格式。

4. 原生支持OData

5. 支持Self-host或者IIS host。

6. 支持大多数MVC功能,例如Routing/Controller/Action Result/Filter/Model Builder/IOC Container/Dependency Injection。

Web API vs MVC

你可能会觉得Web API 与MVC很类似,他们有哪些不同之处呢?先上图,这就是他们最大的不同之处。

Picture1

详细点说他们的区别,

  • MVC主要用来构建网站,既关心数据也关心页面展示,而Web API只关注数据
  • Web API支持格式协商,客户端可以通过Accept header通知服务器期望的格式
  • Web API支持Self Host,MVC目前不支持
  • Web API通过不同的http verb表达不同的动作(CRUD),MVC则通过Action名字表达动作
  • Web API内建于ASP.NET System.Web.Http命名空间下,MVC位于System.Web.Mvc命名空间下,因此model binding/filter/routing等功能有所不同
  • 最后,Web API非常适合构建移动客户端服务

Web API vs WCF

发布服务在Web API和WCF之间该如何取舍呢?这里提供些简单地判断规则,

  • 如果服务需要支持One Way Messaging/Message Queue/Duplex Communication,选择WCF
  • 如果服务需要在TCP/Named Pipes/UDP (wcf 4.5),选择WCF
  • 如果服务需要在http协议上,并且希望利用http协议的各种功能,选择Web API
  • 如果服务需要被各种客户端(特别是移动客户端)调用,选择Web API

Web API 实战 (Web API + MongoDB + knockoutjs)

ASP.NET网站上有很多简单的Web API实例,看看贴图和实例代码你就明白怎么用了。这里我们通过一个稍微复杂一点的实例来展示下Web API的功能。

涉及技术

在我们的实例里面用到了:

服务URI Pattern

Action Http verb URI
Get contact list GET /api/contacts
Get filtered contacts GET /api/contacts?$top=2
Get contact by ID GET /api/contacts/id
Create new contact POST /api/contacts
Update a contact PUT /api/contacts/id
Delete a contact DELETE /api/contacts/id

准备工作

1. 下载并安装Mongo DB,步骤看这里

2. Mongo DB C# driver下载可以在nuget搜索mongocsharpdriver。

3. 如果想本地察看数据库中内容,下载MongoVUE

4. Knockoutjs下载可以在nuget搜索knockoutjs。

代码实现

1. 创建项目

创建MVC4 Web Application

1

在Project Template中选择Web API

2

然后项目就创建成了,Controllers里面有一个ValuesController,是自动生成的一个最简单的Web API Controller。

正如我们前面所说,里面引用的是System.Web.Http命名空间。

3

2. 创建model

在model里面添加Contact类

4

代码如下,其中BsonId需要mongocsharpdriver。

1
2
3
4
5
6
7
8
9
public class Contact
    {
        [BsonId]
        public string Id { get; set; }
        public string Name { get; set; }
        public string Phone { get; set; }
        public string Email { get; set; }
        public DateTime LastModified { get; set; }
    }

我们需要添加mongosharpdriver。

7

8

另外我们需要在Model中添加Repository,Controller通过该类来访问Mongo DB。

1
2
3
4
5
6
7
public interface IContactRepository {
        IEnumerable GetAllContacts();
        Contact GetContact(string id);
        Contact AddContact(Contact item);
        bool RemoveContact(string id);
        bool UpdateContact(string id, Contact item);  
    }

ContactRepository的完整实现如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public class ContactRepository : IContactRepository
    {
        MongoServer _server = null;
        MongoDatabase _database = null;
        MongoCollection _contacts = null;
        public ContactRepository(string connection)
        {
            if (string.IsNullOrWhiteSpace(connection))
            {
                connection = "mongodb://localhost:27017";
            }
            _server = new MongoClient(connection).GetServer();
            _database = _server.GetDatabase("Contacts");
            _contacts = _database.GetCollection("contacts");
            // Reset database and add some default entries
            _contacts.RemoveAll();
            for (int index = 1; index < 5; index++)
            {
                Contact contact1 = new Contact
                {
                    Email = string.Format("test{0}@example.com", index),
                    Name = string.Format("test{0}", index),
                    Phone = string.Format("{0}{0}{0} {0}{0}{0} {0}{0}{0}{0}", index)
                };
                AddContact(contact1);
            }
        }
        public IEnumerable GetAllContacts()
        {
            return _contacts.FindAll();
        }
        public Contact GetContact(string id)
        {
            IMongoQuery query = Query.EQ("_id", id);
            return _contacts.Find(query).FirstOrDefault();
        }
        public Contact AddContact(Contact item)
        {
            item.Id = ObjectId.GenerateNewId().ToString();
            item.LastModified = DateTime.UtcNow;
            _contacts.Insert(item);
            return item;
        }
        public bool RemoveContact(string id)
        {
            IMongoQuery query = Query.EQ("_id", id);
            WriteConcernResult result = _contacts.Remove(query);
            return result.DocumentsAffected == 1;
        }
        public bool UpdateContact(string id, Contact item)
        {
            IMongoQuery query = Query.EQ("_id", id);
            item.LastModified = DateTime.UtcNow;
            IMongoUpdate update = Update
                .Set("Email", item.Email)
                .Set("LastModified", DateTime.UtcNow)
                .Set("Name", item.Name)
                .Set("Phone", item.Phone);
            WriteConcernResult result = _contacts.Update(query, update);
            return result.UpdatedExisting;
        }
    }

3. 添加Controller

右键Controllers目录选择添加Controller

5

选择Empty API controller,将Controller命名为ContactsController

6

添加如下代码,可以看到Controller中的API方法名就是以http verb命名的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class ContactsController : ApiController
    {
        private static readonly IContactRepository _contacts = new ContactRepository(string.Empty);
        public IQueryable Get()
        {
            return _contacts.GetAllContacts().AsQueryable();
        }
        public Contact Get(string id)
        {
            Contact contact = _contacts.GetContact(id);
            if (contact == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
            return contact;
        }
        public Contact Post(Contact value)
        {
            Contact contact = _contacts.AddContact(value);
            return contact;
        }
        public void Put(string id, Contact value)
        {
            if (!_contacts.UpdateContact(id, value))
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
        }
        public void Delete(string id)
        {
            if (!_contacts.RemoveContact(id))
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
        }
    }

4. 添加View

首先添加Knockoutjs库,

9

Knockoutjs通过MVVM模式来实现动态html绑定数据,如下图,其中View-Model是客户端的JavaScript object保存的model数据。

webapi_ef16

先打开HomeController,里面添加一个新的Action代码如下,因为我们要在MVC中对于ContactsController添加对应的View。

1
2
3
4
5
6
7
public ActionResult Admin()
        {
            string apiUri = Url.HttpRouteUrl("DefaultApi", new { controller = "contacts", });
            ViewBag.ApiUrl = new Uri(Request.Url, apiUri).AbsoluteUri.ToString();
            return View();
        }

然后右键Admin方法,选择添加View

10

选择Create strongly-typed view,在model class中选择Contact类。

11

添加View的完整代码,注意view中我们通过js去访问WebAPI,以及通过动态绑定将数据呈现在网页上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
@model WebAPIDemo.Models.Contact
@{
    ViewBag.Title = "Admin";
}
@section Scripts {
  @Scripts.Render("~/bundles/JQueryval")
  <script type="text/JavaScript" src="@Url.Content("~/Scripts/knockout-2.3.0.js")"></script>
  <script type="text/javascript">
      function ProductsViewModel() {
          var self = this;
          self.products = ko.observableArray();
          var baseUri = '@ViewBag.ApiUrl';
          self.create = function (formElement) {
              // If valid, post the serialized form data to the web api
              $(formElement).validate();
              if ($(formElement).valid()) {
                  $.post(baseUri, $(formElement).serialize(), null, "json")
                      .done(function (o) { self.products.push(o); });
              }
          }
          self.update = function (product) {
              $.ajax({ type: "PUT", url: baseUri + '/' + product.Id, data: product });
          }
          self.remove = function (product) {
              // First remove from the server, then from the UI
              $.ajax({ type: "DELETE", url: baseUri + '/' + product.Id })
                  .done(function () { self.products.remove(product); });
          }
          $.getJSON(baseUri, self.products);
      }
      $(document).ready(function () {
          ko.applyBindings(new ProductsViewModel());
      })
  </script>
}
<h2>Admin</h2>
<div class="content">
    <div class="float-left">
    <ul id="update-products" data-bind="foreach: products">
        <li>
            <div>
                <div class="item">ID</div> <span data-bind="text: $data.Id"></span>
            </div>
            <div>
                <div class="item">Name</div>
                <input type="text" data-bind="value: $data.Name"/>
            </div>
            <div>
                <div class="item">Phone</div>
                <input type="text" data-bind="value: $data.Phone"/>
            </div>
            <div>
                <div class="item">Email</div>
                <input type="text" data-bind="value: $data.Email"/>
            </div>
            <div>
                <div class="item">Last Modified</div> <span data-bind="text: $data.LastModified"></span>
            </div>
            <div>
                <input type="button" value="Update" data-bind="click: $root.update"/>
                <input type="button" value="Delete Item" data-bind="click: $root.remove"/>
            </div>
        </li>
    </ul>
    </div>
    <div class="float-right">
    <h2>Add New Product</h2>
    <form id="addProduct" data-bind="submit: create">
        @Html.ValidationSummary(true)
        <fieldset>
            <legend>Contact</legend>
            @Html.EditorForModel()
            <p>
                <input type="submit" value="Save" />
            </p>
        </fieldset>
    </form>
    </div>
</div>

接下来在_layout.cshtml中添加一个admin页面的链接如下

1
2
3
4
5
6
<ul id="menu">
    <li>@Html.ActionLink("Home", "Index", "Home", new { area = "" }, null)</li>
    <li>@Html.ActionLink("API", "Index", "Help", new { area = "" }, null)</li>
    <li>@Html.ActionLink("Admin", "Admin", "Home")</li>
</ul>

5. 测试与调试

大功告成,直接运行下我们的作品,我们的admin链接也显示在右上角,

12

Admin页面的样子,Contact list是动态加载进来的,可以通过这个页面做添加,修改,删除的操作。

13

通过IE network capture来查看请求内容,

重新加载页面,可以看到回复的格式为JSON,

14

JSON内容就是我们mock的一些数据。

image

接下来我们修改,删除,又添加了一条记录,可以看到使用了不同的http method。

image

通过前面安装的mongovue来查看下DB种的数据,先添加的user也在其中,令我感到欣慰。。。

image

其实还有两个有趣的实例,不过文章一写就长了,不好意思耽误大家时间,只好先放放,以后再写。

阿里云ECS(Ubuntu)单节点Kubernetes部署——小白教程 - LifeOfCoding - 博客园

mikel阅读(1068)

来源: 阿里云ECS(Ubuntu)单节点Kubernetes部署——小白教程 – LifeOfCoding – 博客园

目录

参考资料

前言

准备

环境、工具

kubeadm、kubectl、kubelet简介

安装kubeadm、kubectl、kubelet

部署前准备

开始部署
查看部署状态
安装网络插件
测试
排错工具
友情链接

参考资料:


前言

这篇文章是比较久之前写的了,无聊翻了下博客发现好几篇博文排版莫名其妙的变了… 于是修改并完善了下。当初刚玩k8s的时候真的是踩坑踩到怕,官方的指引很多不能直接使用,然后百度上找到的很多资料都是直接搬运官网ε=(´ο`*)))唉。 最后还是硬着头皮百度谷歌了很久才算入了门。在这里写下这篇文章帮助下其他孩纸。

准备

在撸起袖子开干之前最好是对k8s有点基本的认识,比如什么是kubelet,啥是kubectl,配置文件又是什么东东之类的基础。可以翻看我其它有关k8s的文章简单了解下,虽然写得不好,但也不会很辣眼睛。有能力的还是到谷歌和官网看看(看英文版)。

环境、工具

阿里云学生机ECS、Ubuntu、docker、kubectl1.15.4、kubelet1.15.4、kubeadm1.15.4、


kubeadm、kubectl、kubelet简介

kubeadm

用于部署k8s的工具很多,kubeadm就是其中之一,因为比较简单、好上手所以用的比较多,这是一个命令行的工具,可以通过一条简单的命令就把k8s部署好。

kubectl

kubectl也是一个命令行工具,用来管理集群。在用kubeadm工具部署好集群环境后,几乎所有的操作都是通过kubectl来完成的。啥操作?那多了去咯,譬如在k8s中运行一个应用、部署应用服务、查看集群环境等等。最基本的一条命令就是

kubectl create

这条命令用于创建资源,什么是资源?k8s集群中所有东西都可以看做是资源,如用户凭证,用户角色以及用于 ”存放“ 容器的pod等。

kubectl create -f xxx.yaml

如上命令就是用配置文件创建资源的例子。”-f“ 用来指定配置文件,”xxx.yaml“就是配置文件,配置文件可以是yaml格式也可以是json,不过yaml用的比较多。配置文件基本上就是对资源属性的描述,并不难。
配置文件可以是本地文件也可以是网络上的文件,k8s官网的很多配置文件都是直接引用”raw.githubusercontent.com“的内容。当然,不用配置文件,直接使用命令行也是可以创建一些简单资源的:

kubectl create serviceaccount admin-user -n kubernetes-dashboard

这就创建了一个serviceaccount资源。

kubelet

kubelet是一个自动管理、调度集群资源的程序,不需要像kuectl一样手动去操纵它从而完成某些需求,只要懂得如何进行配置,如何查看日志即可。kubelet的配置项非常非常多,但是作为新手不用太过于关注,简单了解即可。

安装kubeadm、kubectl、kubelet

配置软件源

默认apt软件源里没有这几个软件,需要添加谷歌官方的软件源。但是因为官方提供的源无法访问,所以这里改成阿里的源:

curl -s https://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg | apt-key add -
cat <<EOF >/etc/apt/sources.list.d/kubernetes.list
deb https://mirrors.aliyun.com/kubernetes/apt/ kubernetes-xenial main
EOF
apt-get update

命令说明:
1.通过下载工具下载位于https://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg的deb软件包密钥,然后通过”apt-key”命令添加密钥
2.通过cat把源deb https://mirrors.aliyun.com/kubernetes/apt/ kubernetes-xenial main写入到”/etc/apt/sources.list.d/kubernetes.list”

*此处下载工具使用curl,若未安装,先执行如下命令安装。“apt-transport-https”工具允许apt通过https来下载软件,可以不用安装这个,只装curl

apt-get update && apt-get install -y apt-transport-https curl

完成以上步骤,可通过”apt-key list”命令看到类似如下的密钥信息:

查看”/etc/apt/sources.list.d/kubernetes.list”,如下:

选择软件版本

kubeadm、kubectl、kubelet三者的版本要一致,否则可能会部署失败,小版本号不同倒也不会出什么问题,不过尽量安装一致的版本。记住kubelet的版本不可以超过API server的版本。例如1.8.0的API server可以适配 1.7.0的kubelet,反之就不行了。
可以通过”apt-cache madison”命令来查看可供安装的软件的版本号
例:

apt-cache madison kubeadm kubelet kubectl

开始安装

这里安装的版本是”1.15.4-00″,别忘了后面的”-00″。
需要注意,安装kubeadm的时候,会自动安装kubectl、kubelet和cri-tool,安装kubelet时则会自动安装kubernetes-cni,如下:

然而这并不是一件好事,仔细看会发现自动安装的kubectl和kubelet是最新版本的,与kubeadm版本不一致。。。
所以应该更改安装顺序,先安装kubectl和kubelet,命令如下:

apt-get install kubectl=1.15.4-00 kubelet=1.15.4-00 kubeadm=1.15.4-00

如果不想让软件更新,可以输入:

apt-mark hold kubeadm kubectl kubelet

允许更新:

apt-mark unhold kubeadm kubectl kubelet

部署前准备

关闭防火墙

阿里云ECS的ufw默认是”inactive”,即处于关闭状态,这步可以跳过
在ubuntu下,可以使用”ufw”管理防火墙。
查看防火墙状态:

ufw status 

禁用防火墙:

ufw diable

启用防火墙:

ufw enable

关闭selinux(可跳过)

阿里云ecs没有selinux,这步跳过。如果是其它服务器提供商,如下是网上找到的方法,在此不作验证:

  • 修改/etc/selinux/config文件中设置SELINUX=disabled,然后重启服务器
  • 使用setenforce
    • setenforce 1 设为enforcing模式
    • setenforce 0 设为permissive模式

关闭swap

kubelet1.8+要求把swap关闭,详细原因可以访问Kubernetes 设计分析: 为什么 kubelet 运行时不能打开 swap?
如果你非要开启swap的话,可以配置”–fail-swap-on flag”参数。

  • 临时修改,重启复原
    • 关闭
    swapoff -a
    
    • 开启
    swapon -a
    
  • 永久修改,重启生效
    1. 把根目录文件系统设为可读写
    sudo mount -n -o remount,rw /
    
    1. 修改”/etc/fstab”文件,在”/swapfile”一行前加#禁用并保存退出重启服务器

配置并开启kubelet服务(可跳过)

貌似安装kubelet的时候会自动配置好。这步可以跳过。

kubelet --network-plugin=cni
systemctl enable kubelet

修改Docker的cgroup-driver

docker的cgroup-driver应该与kubelet的一致,kubelet默认为“systemd”。

方法一,修改docker的cgroup-driver为systemd

编辑”/etc/docker/daemon.json”(如果没有这个文件就自己新建一个)
添加如下信息:

"exec-opts": ["native.cgroupdriver=systemd"]

注意,如果之前添加了其它的镜像源,或者添加了其它参数,需要在其它配置项后用“,”逗号隔开,再添加新的配置项,否则配置文件会解析失败。
修改完后保存,重新载入配置文件,重启docker

systemctl daemon-reload
systemctl restart docker

方法二,修改kubelet的cgroup-driver为cgroupfs

修改”/etc/systemd/system/kubelet.service.d/10-kubeadm.conf”文件(该文件在安装kubelet时生成),在”Environment=”KUBELET_KUBECONFIG_ARGS=”后增加–cgroup-driver=cgroupfs

Environment="KUBELET_KUBECONFIG_ARGS=--bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf --cgroup-driver=cgroupfs"

配置iptable转发规则

docker 1.13+ 版本,启动后默认把iptable的FORWARD策略改为DROP,可能会影响k8s集群报文转发功能,需要修改FORWARD链的默认策略为ACCEPT。

  • 查看iptables策略
iptables -vnL
  • 临时修改
iptables -P FORWARD ACCEPT
  • 永久修改
    修改docker的service文件,文件路径”/lib/systemd/system/docker.service”,添加
ExecStartPost=/sbin/iptables -P FORWARD ACCEPT  # 当docker启动后会额外执行此条命令。此行加到ExecStart行下面。

重启docker

systemctl daemon-reload
systemctl start docker
systemctl enable docker

开始部署

kubeadm init --kubernetes-version=v1.15.4 --ignore-preflight-errors=NumCPU --pod-network-cidr=10.244.0.0/16 --image-repository registry.aliyuncs.com/google_containers

参数说明:

  • “kubernetes-version”:指定k8s的版本,对于不同版本k8s,kubeadm会去拉取不同版本的镜像。
  • “–pod-network-cidr”: 指定使用”cidr”方式给pod分配IP,参数的数值必须跟接下来要安装的网络插件flannel的配置一致, 否则flannel部署失败。后面用到的网络插件为flannel,flannel配置文件中默认的数值是“10.244.0.0/16”。
  • “–image-repository”: 指定镜像仓库,kubeadm默认的仓库地址没法访问,需要指定为阿里云的地址——”registry.aliyuncs.com/google_containers”
  • “–ignore-preflight-errors”:kubeadm在初始化之前,会执行“preflight”(预检),当条件不满足时会报错并停止初始化进程,但是有些报错并不会影响集群的初始化,因此可以指定该参数忽略特定的错误。阿里云学生机cpu只有单核,而k8s要求双核及以上,所以需要指定参数”–ignore-preflight-errors=NumCPU”。
    该参数的用法如下:
--ignore-preflight-errors=<option>


<option>就是错误的类型,如上图所示,错误提示是”[ERROR NumCPU]”,那么参数就写成:

--ignore-preflight-errors=NumCPU

查看部署状态


当看到上述信息就表示集群Master节点初始化成功,在同一网络下的机器上同样地安装kuneadm、kubelet并配置好环境之后,即可通过”kubeadm join”命令连接到Master节点使集群成为多节点集群:

kubeadm join 192.168.1.73:6443 --token gkp9ws.rv2guafeusg7k746 \
    --discovery-token-ca-cert-hash sha256:4578b17cd7198a66438b3d49bfb878093073df23cf6c5c7ac56b3e05d2e7aec0

该token默认有效期为24小时,可通过”kubeadm token create –print-join-command”命令创建新token,并打印连接命令:


初始化成功后,在master节点上可以通过kubectl命令来查看集群上资源的状态,但是kubectl在访问API Server时需要认证,否则会出现”The connection to the server localhost:8080 was refused”这样的错误。有如下两种解决方法:
一、
认证信息在”/etc/kubernetes/admin.conf”文件里,所以需要把文件拷贝到”$HOME/.kube/config/”下,

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

这段命令会在初始化成功后打印出来,直接拷贝即可。

二、
也可以直接添加环境变量指定文件地址:

export KUBECONFIG=/etc/kubernetes/admin.conf

可以把这条变量添加到”/etc/profile”,然后”source /etc/profile”。

配置好后,通过”kubectl get node”即可看到集群的所有节点状态:

可以看到当前节点处于”NotReady”状态,通过”kubectl describe node <your_node>”命令查看node情况,<your_node>替换成自己的节点名,此时可以看到这样的信息:

在”Ready”一行,”status”为”false”,”message”提示”Runtime newtwork not ready”,意思是网络插件未准备好,所以此时应该给集群安装网络插件。


安装网络插件

网络插件有很多种,此处选择”flannel”,flannel的安装比较简单,直接指定配置文件,用”kubectl”安装即可。配置文件如下:

flannel.yaml

博客园markdown的details标签不好用,格式可能有点问题,给个github的传送门


128行的network数值应与”kubeadm”初始化时指定的”–pod-network-cidr”一致,该值表示给pod分配ip时的ip前缀。

创建好配置文件后,通过”kubeclt”安装flannel:

kubectl create -f <flannel_yaml_path>

把<flannel_yaml_path>替换成自己的配置文件路径,执行命令后稍等片刻,单节点k8s集群就部署好了。


测试

处理taint

默认情况下,出于安全考虑,master节点会被打上一个叫”NoSchedule”的Taint(污点),可以通过”kubectl describe”看到:

这个taint使得master节点不能被调度,也就是说master节点不能部署应用,由于现在搭建的是单节点集群,当前节点既充当master又得充当worker,所以需要把这个taint去掉:

kubectl taint node <node_name> <taint>-

<node_name>替换成自己节点名称,<taint>替换成taint,如:

kubectl taint node yo node-role.kubernetes.io/master:NoSchedule-

注意别忘了taint后面的横杠”-“,”-“表示“减号”,即把taint去掉。如果想把”taint”加回去,只需要把”-“去掉,并在”:”前加上”=”,如”kubectl taint node yo node-role.kubernetes.io/master=:NoSchedule”

运行nginx:

docker pull nginx
kubectl run nginx --image=nginx

稍等片刻nginx就部署好了,可以通过”kubectl get pods –all-namespaces”查看,或者直接访问”curl localhost:80″。


排错工具

kubectl

kubectl get <resource_type>

“kubectl get”可以列出集群环境中的某类资源,对于k8s,几乎所有内容都是“资源”,如Node、Pod、Service等,只要把”<resource_type>”替换成想查看的资源类型即可。
如查看节点资源:

kubectl get node


对于pod等其它的资源,kubectl的用法会有些许不同。k8s使用”namespace”来对集群中的资源进行分组管理,可以把”namespace”当做“分组的名称”,也可以把”namespace”当做k8s集群中的”集群”,不同”namespace”的资源在逻辑上彼此隔离,以此提高安全性,提高管理效率。用kubeectl查看这些个资源时,需要用”-n”来指定”namespace”,如:

kubectl get pod -n kube-system


还可以用”–all-namespaces”来查看所有”namespaces”下的资源:

kubectl get pod --all-namespaces


kubectl describe <resource_type> <resource_name>

对于处于异常状态的资源,可以使用该命令查看其详细信息,只要把<resource_type>替换成资源类别,把<resource_name>替换成资源名称即可,当然也还需要用”-n”指明”namespaces”。
如:

kubectl describe pod -n kubernetes-dashboard kubernetes-dashboard-6b855d4584-9sgsk

然后就可以看到该pod的事件信息:


docker ps -a

该命令可以查看当前所有docker容器,”-a”表示所有容器,当不加该参数时,显示的则是正在运行的容器。由于要查看的是k8s相关的容器,以此可以使用管道命令和”grep”对显示结果进行筛选:

docker ps -a | grep kube

对于处于”Exited”状态的异常容器,使用”docker logs <container_id>”命令查看容器日志。如:

docker logs 37443d902aee

此处”37443d902aee”是我机器上”kubernetes-dashboard”的容器id。


友情链接

一小时快速搭建基于阿里云容器服务-Kubernetes的Web应用 - zhaowei121 - 博客园

mikel阅读(720)

来源: 一小时快速搭建基于阿里云容器服务-Kubernetes的Web应用 – zhaowei121 – 博客园

本文面向的读者

如果您是一个Kubernetes的初学者,本文可以帮助你快速在云上搭建一个可实际使用的集群环境,并发布自己的第一个应用。你无须提前准备任何的硬件资源或者下载任何的软件包。
如果您已经有一个自建的Kubernetes集群,想要尝试阿里云上的托管集群,本文可以帮助你快速完成上手操作,而无需详细阅读阿里云的帮助文档,从而节省您的时间。您可以在有了端到端的初体验之后,再有选择的阅读容器服务和容器镜像服务的帮助文档。
如果你已经有一个传统的部署在云上的Web应用(比如部署在云上的ECS里),想要进行容器化的改造,本文同样可以帮助到您,您甚至无需深入学习Kubernetes,只需了解基本概念即可。

准备代码

本文的操作全部基于阿里云控制台,因此您只需要一个阿里云控制台的登录账号即可。

我们先把应用的代码准备好。请登录https://code.aliyun.com/ ,登录完成后,访问https://code.aliyun.com/shengbo.tsb/yunputest ,点击派生项目(fork)的图标。

在随后弹出的确认框里,点击头像确认,完成派生。

备选方案:如果您派生遇到了困难,可以直接从https://github.com/docker-training/webapp clone这个项目,然后自己通过git push到code.aliyun.com上。

准备镜像仓库

登录https://cr.console.aliyun.com/cn-beijing/instances/repositories
在左侧导航栏选择“命名空间”

点击“创建命令空间”,输入名字“tengshengbo”。

确认命名空间创建完成之后,点击左侧导航栏“镜像仓库”,点击“创建镜像仓库”,输入仓库名称“yunputest”

在下一步,选择刚刚建立的代码仓库。选择“海外机器构建”以加快构建速度。提示:如果下拉框没有代码仓库,点击右侧的刷新按钮。

创建新镜像

回到https://code.aliyun.com/ 点击yunputest这个项目,注意,是自己名下的项目,而不是shengbo.tsb这个账号名下的项目。 点击“新标签”.

输入标签名release-v1.5。注意,您必须使用release-v[版本号]形式,比如release-v2.5或者其他。

返回镜像仓库列表,https://cr.console.aliyun.com/cn-beijing/instances/repositories

选择自己的镜像仓库,左侧导航选择“构建”,可以看到一个构建自动生成了。

创建Kubernetes集群

访问容器服务控制台 https://cs.console.aliyun.com/#/k8s/cluster/list

选择“创建Kubernetes集群”, 集群模板选择第一个“标准托管集群”

集群名称选择“yunpu-k8s”,选择专有网络和虚拟交换机,勾选公网访问(为了开发测试方便),选择实例类型(如果不考虑性能,单纯为了省钱,可以使用t5或者t6实例),其他默认就好。

注意:Pod CIDR,Service CIDR不能与VPC内已有网段冲突,如果提示有错误,请更换网络的CIDR。

在确认对话框里勾选协议,完成创建。之后,确认自己的集群运行中。

部署第一个应用

容器服务的左侧导航栏选择“应用-无状态”,点击“使用镜像创建”,在“应用基本信息”这一部分, 输入应用名称yunpuapp, 选择刚创建的集群yunpu-k8s, 其他默认。

下一步,容器配置,镜像名称一项,点击“选择镜像”,选择刚才自动从代码创建的镜像

镜像Tag一项,点击“选择镜像Tag”,选择你刚刚自己定义的版本号,比如1.4。其他保持默认。

点击下一步。

在高级配置里面,点击“服务(Service)”旁边的“创建”,

端口映射一项,输入名称port,注意服务端口80,容器端口5000.

点击“路由(Ingress)”旁边的“创建”,输入域名yunpuapp选择刚刚创建的服务以及port。

创建成功之后,可以看到应用的访问方式,在“路由(Ingress)”里面,“规则”一项,有一个链接。

点击这个链接,可以访问应用,显示内容包含响应时间、应用运行时的host name以及IP地址。多次刷新链接,可以看到不同的ip地址,这个说明后面对应的容器组Pod是多个。

升级应用

建议您自行尝试更改应用的代码,比如简单更改webapp/app.py这个文件,完成git tag后,git push.

或者,您也可以直接在 https://code.aliyun.com/ 控制台上,为master分支新建标签release-v1.5

等待镜像更新完成后,在“无状态应用”页,点击应用旁边的“编辑”。

编辑页面,选择新的版本1.5,点击更新

下一步

阿里云的容器服务Kubernetes不是只有托管版,还有专有版和Serverless版。专有版适合有丰富的容器运维经验,希望长时间运行某个应用,并且独立自主运维全部基础设施的的大企业用户。Serverless版适合不希望关心基础设施的运维,或者只是短时间运行某个应用从而希望开箱即用的中小企业用户。本文介绍的托管版则介于专有版和Serverless版两者之间。

如果您想了解更多,建议参考以下资料:
容器服务Kubernetes版帮助文档: https://help.aliyun.com/product/85222.html
容器镜像服务帮助文档: https://help.aliyun.com/product/60716.html

如果您需要针对容器服务进行运维,参考以下资料:
运维编排:
https://help.aliyun.com/product/119529.html
资源编排:
https://help.aliyun.com/product/28850.html

 

本文作者:云普

原文链接

本文为阿里云内容,未经允许不得转载。

基于阿里云 Terway 网络的 Kubernetes 集群实践 - k8s-kb - 博客园

mikel阅读(768)

来源: 基于阿里云 Terway 网络的 Kubernetes 集群实践 – k8s-kb – 博客园

作者:BGbiao ,来源:简书,原文链接

背景:众所周知的是在构建一个Kubernetes集群时,容器网络通常会使用一个独立的私有子网来构建Kubernetes集群内部的pod网络和service网络,但在实际的业务场景中,没有企业会在一段时间内将内部全部的服务都迁移到Kubernetes集群中(因为涉及到业务架构以及整体业务的可靠性),因而会产生一些Kubernetes集群内部服务和集群外部服务互相调用的场景,当然如果是HTTP服务,我们可以采用LVS、Nginx、HAProxy之类的代理工具工具进行集群内外的流量转发,但如果是TCP服务,比如使用Dubbo框架时,生产者和消费者需要直连,当生产者和消费者不在一个可以互联互通的网络下会比较麻烦,这也就是为什么大厂在规模化使用Kubernetes时首先需要解决的就是网络问题的原因了。比如我们在数科的时候就采用的是Contiv+BGP的模式来实现容器网络和容器外网络的互联互通的,而这通常需要一个比较专业的SDN团队来构建和维护。而作为创业公司通常会使用公有云来承载自己的业务,这种轻资产模式的好处就是底层会有专业的团队来提供保障,因此考虑到业务需求我们采用了阿里云的terway网络插件来实现内部的Kubernetes集群网络.

现有网络插件

  • Flannel: Flannel是最早CoreOS团队开源的网络插件,用于让集群中不同节点创建的容器都具有集群内全局唯一的网络(集群外无法感知),也是当前Kubernetes开源方案中比较成熟的方案,支持HostGW和VXLAN模式
  • Calico: Calico是一个纯3层的数据中心网络方案,支持IPIP和BGP模式,后者可以无缝集成像OpenStack这种IaaS云架构,能够提供可控的VM、容器、裸机之间的IP通信,但是需要网络设备对BGP的支持(阿里云vpc子网内应该是不支持BGP的); 同时可以支持基于iptables的网络策略控制
  • Contiv: Contiv是思科开源的用于跨虚拟机、裸机、公有云或私有云的异构容器部署的开源容器网络架构,可支持2层、3层网络(通常也需要BGP的支持)
  • Terway: Terway是阿里云开源的基于VPC网络的CNI插件,支持VPC和ENI模式,后者可实现容器网络使用vpc子网网络

以上就是当前开源Kubernetes集群中使用较多的集中网络方案,我们的业务需求中也是需要打通容器内外的网络,因此在成本、效率以及稳定性上优先选择采用阿里云的Terway网络方案来满足我们的Kubernetes集群需求.

基于阿里云ECS搭建Terway网络的Kubernetes集群

注意: 阿里云容器服务ACK默认也支持两种网络,Flannel和Terway,前者和开源插件基本一致,后者支持VPC模式和ENI模式,VPC模式可实现容器网络使用vpc内交换机子网地址,但是默认无法和其他交换机下的ecs主机通信,ENI模式会给pod容器组分配一块弹性网卡来实现和集群外网络的互联互通,但Terway网络下的ENI模式需要部分特殊机型才可以支持。

由于ACK下Terway的ENI模式对机型的要求,我们采用购买ECS来自己搭建单节点集群测试Terway网络下容器的互联互通.

前提条件:

  • 已经创建了VPC子网
  • 在VPC子网下创建2个虚拟交换机(模拟Kubernetes集群网络和ECS网络)
  • 分别在两个子网购买两台ECS主机(模拟ECS到容器的互联互通)

注意: Terway网络插件官方验证过的os镜像为Centos 7.4/7.6,购买ecs时需要注意

1. 使用kubeadm安装k8s单节点集群

注意: 因为要使用terway网络将pod和ecs网络打通,因此需要将内核参数rp_filter全部设置为0(对数据包源地址不进行校验)

复制代码
# 更新yum源并安装k8s相关组件
$ yum update
$ cat <<EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=http://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=0
repo_gpgcheck=0
gpgkey=http://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg
        http://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg
EOF

$ yum clean all
$ yum install kubelet kubeadm kubectl --disableexcludes=kubernetes -y
$ yum install docker -y


# 启动kubelet
## 此时kubelet会无限重试,因为会链接apiserver
$ systemctl restart kubelet

# 启动docker
## 注意:需要注意kubelet中的cgroupfs类型要和docker的cgroupfs一致
$ systemctl restart docker

# 查看kubeadm 启动集群时所需镜像
# 注意:kubeadm默认使用的是谷歌的镜像仓库,可将镜像仓库换成阿里云镜像仓库
# 将k8s.gcr.io 替换成registry.cn-hangzhou.aliyuncs.com/google_containers 即可
$ kubeadm config images list
k8s.gcr.io/kube-apiserver:v1.16.2
k8s.gcr.io/kube-controller-manager:v1.16.2
k8s.gcr.io/kube-scheduler:v1.16.2
k8s.gcr.io/kube-proxy:v1.16.2
k8s.gcr.io/pause:3.1
k8s.gcr.io/etcd:3.3.15-0
k8s.gcr.io/coredns:1.6.2

# 初始化集群
## 注意:初始化时需要指定vpc的子网,否则后期可能会发现无法识别vpc子网
$ kubeadm  init  --pod-network-cidr=172.16.48.0/20
....
....
Your Kubernetes control-plane has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

  mkdir -p $HOME/.kube
  sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
  sudo chown $(id -u):$(id -g) $HOME/.kube/config

You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
  https://kubernetes.io/docs/concepts/cluster-administration/addons/

Then you can join any number of worker nodes by running the following on each as root:

kubeadm join 172.16.62.70:6443 --token j4b3xp.78izi2bmitxxx \
    --discovery-token-ca-cert-hash sha256:fd1ff50cbabd4fb22cb9a866052fbdc0db7da662168cda702exxxxxxxx

# 接下来按照上述提示创建配置文件
$ mkdir -p $HOME/.kube
$ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
$ sudo chown $(id -u):$(id -g) $HOME/.kube/config

# 查看k8s的node节点(当前处于NotReady状态,因为kubelet还没有成功启动)
$ # kubectl version
Client Version: version.Info{Major:"1", Minor:"16", GitVersion:"v1.16.2", GitCommit:"c97fe5036ef3df2967d086711e6c0c405941e14b", GitTreeState:"clean", BuildDate:"2019-10-15T19:18:23Z", GoVersion:"go1.12.10", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"16", GitVersion:"v1.16.2", GitCommit:"c97fe5036ef3df2967d086711e6c0c405941e14b", GitTreeState:"clean", BuildDate:"2019-10-15T19:09:08Z", GoVersion:"go1.12.10", Compiler:"gc", Platform:"linux/amd64"}

$ kubectl  get nodes
NAME                      STATUS     ROLES    AGE     VERSION
izbp18diszrt8m41b2fbpsz   NotReady   master   7m19s   v1.16.2
复制代码

2. 给k8s集群创建terway网络

注意: 使用kubeadm创建的k8s集群是v1.16的,官方提供的yaml文件中需要稍微修改下DaemonSet的相关部分。

复制代码
# 给集群创建k8s的cni网络插件,也就是前面说的terway插件
# 需要修改阿里云相关的配置(ak,as,subnet,security_group)
$ curl -O https://raw.githubusercontent.com/BGBiao/k8s-ansible-playbooks/master/manifest/cni/terway/podnetwork.yaml

# 修改podnetwork.yaml中的配置(指定阿里云的ak和as认证信息以及vpc子网和安全组信息)
$ cat podnetwork.yaml
...
...
  eni_conf: |
    {
      "version": "1",
      "access_key": "your ak",
      "access_secret": "your as",
      "service_cidr": "your vpc subnet",
      "security_group": "your 安全组id",
      "max_pool_size": 5,
      "min_pool_size": 0
    }
....
....
          - name: Network
            value: "your vpc subnet"
....

# 创建terway网络
$ kubectl apply -f podnetwork.yaml
serviceaccount/terway created
clusterrole.rbac.authorization.k8s.io/terway-pod-reader created
clusterrolebinding.rbac.authorization.k8s.io/terway-binding created
configmap/eni-config created
daemonset.apps/terway created
customresourcedefinition.apiextensions.k8s.io/felixconfigurations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/bgpconfigurations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ippools.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/hostendpoints.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/clusterinformations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/globalnetworkpolicies.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/globalnetworksets.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/networkpolicies.crd.projectcalico.org created

# 查看cni相关容器以及node状态
$ kubectl  get nodes
NAME                      STATUS   ROLES    AGE   VERSION
izbp18diszrt8m41b2fbpsz   Ready    master   28m   v1.16.2

$ kubectl  get pods -A | grep terway
kube-system   terway-b9vm8                                      2/2     Running   0          6m53s
复制代码

至此,我们就已经完成了kubernetes的terway网络单节点集群,接下来就可以尝试让k8s集群中的pod来使用vpc的网络了,以便可以实现k8s集群内部的容器网络和其他ecs主机的网络是平行的.

3. 测试terway网络

注意: 我们使用kubeadm构建的k8s单节点集群,而kubeadm默认给master节点设置了taint,因此测试前需要去除taint。

复制代码
# 去除taint
$ kubectl taint nodes --all node-role.kubernetes.io/master-
node/izbp18diszrt8m41b2fbpsz untainted

# 默认创建一个vpc模式的deployment
$ kubectl  apply -f https://raw.githubusercontent.com/BGBiao/k8s-ansible-playbooks/master/manifest/cni/terway/nginx.yaml
namespace/myapp configured
deployment.apps/nginx-test created

# 可以看到容器网络地址其实是指定的vpc子网内地址
$ kubectl  get pods -n myapp  -o wide
NAME                         READY   STATUS    RESTARTS   AGE     IP            NODE                      NOMINATED NODE   READINESS GATES
nginx-test-d56c87dd9-26mzs   1/1     Running   0          2m40s   172.16.48.5   izbp18diszrt8m41b2fbpsz   <none>           <none>
nginx-test-d56c87dd9-hp2rv   1/1     Running   0          2m40s   172.16.48.4   izbp18diszrt8m41b2fbpsz   <none>           <none>

$ curl 172.16.48.4 -I
HTTP/1.1 200 OK
Server: nginx/1.17.5
Date: Sat, 26 Oct 2019 08:21:28 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 22 Oct 2019 14:30:00 GMT
Connection: keep-alive
ETag: "5daf1268-264"
Accept-Ranges: bytes

$ curl 172.16.48.5 -I
HTTP/1.1 200 OK
Server: nginx/1.17.5
Date: Sat, 26 Oct 2019 08:21:31 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 22 Oct 2019 14:30:00 GMT
Connection: keep-alive
ETag: "5daf1268-264"
Accept-Ranges: bytes
复制代码

可以发现,在集群内部使用terway网络已经没有任何问题了,但是我们在其他ECS主机去访问pod网络时发现依然无法访问(因为默认使用的是terway的VPC模式,其实就是类似于calico的模式了.这个时候就需要用到eni模式了,即给k8s节点增加eni弹性网卡,然后pod的网络流量统一通过node节点的eni网卡传输,此时就可以很好的和整个内网vpc打通了)

4. 测试ENI模式

注意: 在上面的nginx配置中增加limits: aliyun/eni: N即可,需要注意的是N表示node节点上eni弹性网卡的数量,该数量取决于阿里云ecs不同规格对eni的限制。

复制代码
# 注意:
# 由于实验中采用的是4c8g的k8s单节点集群,因此只能创建2个弹性网卡,这也就意味着如果不增加任何网络配置,该node节点最多只能运行2个和整个VPC网络中其他ecs主机互联互通的pod

$ cat nginx.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-v2
  namespace: myapp
spec:
  revisionHistoryLimit: 10
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
  replicas: 2
  selector:
    matchLabels:
      app: nginx-v2
      profile: prod
  template:
    metadata:
      labels:
        app: nginx-v2
        profile: prod
    spec:
      containers:
      - name: nginx-v2
        image: nginx:latest
        imagePullPolicy: IfNotPresent
        resources:
          requests:
            cpu: 200m
            memory: 215Mi
          limits:
            cpu: 200m
            memory: 215Mi
            aliyun/eni: 1


# 创建带eni的pod
$ kubectl  apply -f nginx.yaml
deployment.apps/nginx-v2 configured

# 查看pod状态
$ kubectl  get pods -n myapp -o wide
NAME                         READY   STATUS    RESTARTS   AGE   IP             NODE                      NOMINATED NODE   READINESS GATES
nginx-test-d56c87dd9-26mzs   1/1     Running   0          19m   172.16.48.5    izbp18diszrt8m41b2fbpsz   <none>           <none>
nginx-test-d56c87dd9-hp2rv   1/1     Running   0          19m   172.16.48.4    izbp18diszrt8m41b2fbpsz   <none>           <none>
nginx-v2-7548466fc8-d4klv    1/1     Running   0          61s   172.16.62.74   izbp18diszrt8m41b2fbpsz   <none>           <none>
nginx-v2-7548466fc8-x7ft9    1/1     Running   0          61s   172.16.62.75   izbp18diszrt8m41b2fbpsz   <none>           <none>

# 在k8snode节点访问
$ curl 172.16.62.75 -I
HTTP/1.1 200 OK
Server: nginx/1.17.5
Date: Sat, 26 Oct 2019 08:38:20 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 22 Oct 2019 14:30:00 GMT
Connection: keep-alive
ETag: "5daf1268-264"
Accept-Ranges: bytes

$ curl 172.16.62.74 -I
HTTP/1.1 200 OK
Server: nginx/1.17.5
Date: Sat, 26 Oct 2019 08:38:23 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 22 Oct 2019 14:30:00 GMT
Connection: keep-alive
ETag: "5daf1268-264"
Accept-Ranges: bytes

# 此时发现创建的带eni和不带eni的两个pod在k8s集群内部已经完全可以访问
复制代码

5. 测试集群内外部网络互联互通

注意: k8s集群使用的是vpc网络,因此默认集群访问外部ECS网络默认是没有问题,这里主要测试外部ECS网络是否可以直连pod网络进行通信。

复制代码
# 在同vpc环境下其他ecs主机上访问
# 首先分别ping 上述四个pod的网络(可以发现eni模式下容器默认可以ping通)
$ for i in 172.16.48.5 172.16.48.4 172.16.62.74 172.16.62.75 ;do ping -c 1 -w 1 $i;done
PING 172.16.48.5 (172.16.48.5) 56(84) bytes of data.

--- 172.16.48.5 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 999ms

PING 172.16.48.4 (172.16.48.4) 56(84) bytes of data.

--- 172.16.48.4 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms

PING 172.16.62.74 (172.16.62.74) 56(84) bytes of data.
64 bytes from 172.16.62.74: icmp_seq=1 ttl=64 time=0.782 ms

--- 172.16.62.74 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.782/0.782/0.782/0.000 ms
PING 172.16.62.75 (172.16.62.75) 56(84) bytes of data.
64 bytes from 172.16.62.75: icmp_seq=1 ttl=64 time=0.719 ms

--- 172.16.62.75 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.719/0.719/0.719/0.000 ms

# 测试nginx服务(依然是带eni的网络可达)
$ for i in 172.16.48.5 172.16.48.4 172.16.62.74 172.16.62.75 ;do curl --connect-timeout 1 -I  $i;done
curl: (28) Connection timed out after 1001 milliseconds
curl: (28) Connection timed out after 1001 milliseconds
HTTP/1.1 200 OK
Server: nginx/1.17.5
Date: Sat, 26 Oct 2019 08:44:21 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 22 Oct 2019 14:30:00 GMT
Connection: keep-alive
ETag: "5daf1268-264"
Accept-Ranges: bytes

HTTP/1.1 200 OK
Server: nginx/1.17.5
Date: Sat, 26 Oct 2019 08:44:21 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 22 Oct 2019 14:30:00 GMT
Connection: keep-alive
ETag: "5daf1268-264"
Accept-Ranges: bytes
复制代码

此时,我们查看该node节点上的网卡信息时可以看到,增加了两块辅助网卡。

6. 其他问题

注意:前面我们提到过,如果使用eni模式,不同的ECS规格可以绑定的ENI弹性网卡是有限的,也就是说可以创建互联互通的容器是有限的,我们这里验证下。

复制代码
# 如果我们这个时候再创建带eni的pod时,就会发现无法创建成功(因为4c8g的ecs最大只支持两个eni)
$ kubectl  apply -f nginx-v3.yaml
deployment.apps/nginx-v3 created
[root@iZbp18diszrt8m41b2fbpsZ ~]# kubectl  get pods -n myapp -o wide
NAME                         READY   STATUS    RESTARTS   AGE   IP             NODE                      NOMINATED NODE   READINESS GATES
nginx-test-d56c87dd9-26mzs   1/1     Running   0          48m   172.16.48.5    izbp18diszrt8m41b2fbpsz   <none>           <none>
nginx-test-d56c87dd9-hp2rv   1/1     Running   0          48m   172.16.48.4    izbp18diszrt8m41b2fbpsz   <none>           <none>
nginx-v2-7548466fc8-d4klv    1/1     Running   0          29m   172.16.62.74   izbp18diszrt8m41b2fbpsz   <none>           <none>
nginx-v2-7548466fc8-x7ft9    1/1     Running   0          29m   172.16.62.75   izbp18diszrt8m41b2fbpsz   <none>           <none>
nginx-v3-79dd8fb956-4ghgb    0/1     Pending   0          2s    <none>         <none>                    <none>           <none>
nginx-v3-79dd8fb956-str2k    0/1     Pending   0          2s    <none>         <none>                    <none>           <none>

# 查看Pending的详情
$ kubectl  describe pods -n myapp nginx-v3-79dd8fb956-4ghgb
....
....
Events:
  Type     Reason            Age        From               Message
  ----     ------            ----       ----               -------
  Warning  FailedScheduling  <unknown>  default-scheduler  0/1 nodes are available: 1 Insufficient aliyun/eni.
复制代码

可以发现,当使用terway网络的ENI模式时,如果该ecs可支持的弹性网卡达到限制,k8s就会调度失败。

所以问题就来了,通常情况下,我们是希望使用k8s来弹性扩容,我们会希望k8s节点上运行更多的pod,但用了terway网络之后我们发现,创建和k8s集群外ecs主机通信的pod数量竟然受eni的限制,这可得了?

其实不用担心,阿里云同学的回复是,这种情况下在vpc上设置静态路由即可实现node节点上的多pod和集群外ecs主机互通,此时ecs主机上的eni仅相当于是整个容器的网络出口,到这里其实我们就可以放心了,因为使用terway后,及时不用eni模式,pod网络也是全局唯一的,这个时候适当增加一些静态路由,即可实现整个vpc内k8s容器网络和容器外的ecs主机网络互联互通,很好的解决了我们一开始的问题。

注意:阿里云容器服务ACK的terway网络模式下的集群会默认创建一些路由规则,因此当你使用ACK集群时,只要购买了支持terway规格的节点,默认创建的容器都可以实现和外部ecs主机的互联互通,此时,该ecs上创建的弹性网卡将作为节点上k8s容器的网络出口,而ecs主机本身的eth0将仅作为管理网络而存在,感兴趣的同学可以点击阅读原文尝试使用阿里云ACK的terway网络模式。

.Net微服务实战之Kubernetes的搭建与使用 - 陈珙 - 博客园

mikel阅读(884)

来源: .Net微服务实战之Kubernetes的搭建与使用 – 陈珙 – 博客园

系列文章

前言

说到微服务就得扯到自动化运维,然后别人就不得不问你用没用上K8S。无论是概念上还是在实施搭建时,K8S的门槛比Docker Compose、Docker Swarm高了不少。我自己也经过了多次的实践,整理出一套顺利部署的流程。

我这次搭建花了一共整整4个工作实践与一个工作日写博客,中间有一个网络问题导致reset了集群重新搭了一次,完成后结合了Jenkins使用,还是成就感满满的。如果对大家有用,还请点个推荐与关注。

基本概念

Kubectl

kubectl用于运行Kubernetes集群命令的管理工具,Kubernetes kubectl 与 Docker 命令关系可以查看这里

http://docs.kubernetes.org.cn/70.html

Kubeadm

kubeadm 是 kubernetes 的集群安装工具,能够快速安装 kubernetes 集群,相关命令有以下:

kubeadm init

kubeadm join

Kubelet

kubelet是主要的节点代理,它会监视已分配给节点的pod,具体功能:

  • 安装Pod所需的volume。
  • 下载Pod的Secrets。
  • Pod中运行的 docker(或experimentally,rkt)容器。
  • 定期执行容器健康检查。

Pod

Pod是Kubernetes创建或部署的最小(最简单)的基本单位,一个Pod代表集群上正在运行的一个进程,它可能由单个容器或多个容器共享组成的资源。

一个Pod封装一个应用容器(也可以有多个容器),存储资源、一个独立的网络IP以及管理控制容器运行方式的策略选项。

Pods提供两种共享资源:网络和存储。

网络

每个Pod被分配一个独立的IP地址,Pod中的每个容器共享网络命名空间,包括IP地址和网络端口。Pod内的容器可以使用localhost相互通信。当Pod中的容器与Pod 外部通信时,他们必须协调如何使用共享网络资源(如端口)。

存储

Pod可以指定一组共享存储volumes。Pod中的所有容器都可以访问共享volumes,允许这些容器共享数据。volumes 还用于Pod中的数据持久化,以防其中一个容器需要重新启动而丢失数据。

Service

一个应用服务在Kubernetes中可能会有一个或多个Pod,每个Pod的IP地址由网络组件动态随机分配(Pod重启后IP地址会改变)。为屏蔽这些后端实例的动态变化和对多实例的负载均衡,引入了Service这个资源对象。

Kubernetes ServiceTypes 允许指定一个需要的类型的 Service,默认是 ClusterIP 类型。

Type 的取值以及行为如下:

  • ClusterIP:通过集群的内部 IP 暴露服务,选择该值,服务只能够在集群内部可以访问,这也是默认的 ServiceType。
  • NodePort:通过每个 Node 上的 IP 和静态端口(NodePort)暴露服务。NodePort 服务会路由到 ClusterIP 服务,这个 ClusterIP 服务会自动创建。通过请求 <NodeIP>:<NodePort>,可以从集群的外部访问一个 NodePort 服务。
  • LoadBalancer:使用云提供商的负载局衡器,可以向外部暴露服务。外部的负载均衡器可以路由到 NodePort 服务和 ClusterIP 服务。
  • ExternalName:通过返回 CNAME 和它的值,可以将服务映射到 externalName 字段的内容(例如, foo.bar.example.com)。 没有任何类型代理被创建,这只有 Kubernetes 1.7 或更高版本的 kube-dns 才支持。

其他详细的概念请移步到 http://docs.kubernetes.org.cn/227.html

物理部署图

 

Docker-ce 1.19安装

在所有需要用到kubernetes服务器上安装docker-ce

卸载旧版本 docker

yum remove docker docker-common docker-selinux dockesr-engine -y
升级系统软件
yum upgrade -y
安装必要的一些系统工具
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
添加docker-ce软件源
yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
更新并安装 docker-ce
yum makecache fast
yum install docker-ce-19.03.12 -y
添加docker国内镜像源
vim /etc/docker/daemon.json
 
{
"exec-opts": ["native.cgroupdriver=systemd"],
"registry-mirrors" : [
    "http://ovfftd6p.mirror.aliyuncs.com",
    "http://registry.docker-cn.com",
    "http://docker.mirrors.ustc.edu.cn",
    "http://hub-mirror.c.163.com"
  ],
  "insecure-registries" : [
    "registry.docker-cn.com",
    "docker.mirrors.ustc.edu.cn"
  ],
  "debug" : true,
  "experimental" : true
}

启动服务

systemctl start docker
systemctl enable docker

安装kubernetes-1.18.3

所有需要用到kubernetes的服务器都执行以下指令。

添加阿里kubernetes源
cat <<EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64/
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg https://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg
EOF
安装并启动
yum install kubeadm-1.18.3 kubectl-1.18.3 kubelet-1.18.3

启动kubelet

systemctl enable kubelet
systemctl start kubelet

在Master设置环境变量,在/etc/profile中配置
vim /etc/profile
在最后添加如下配置
export KUBECONFIG=/etc/kubernetes/admin.conf

执行命令使其起效

source /etc/profile

初始化k8s集群

在master节点(server-a)进行初始化集群

开放端口

firewall-cmd --permanent --zone=public --add-port=6443/tcp
firewall-cmd --permanent --zone=public --add-port=10250/tcp
firewall-cmd --reload
关闭swap
vim /etc/fstab
#注释swap那行
 
swapoff -a

设置iptables规则

echo 1 > /proc/sys/net/bridge/bridge-nf-call-iptables
echo 1 > /proc/sys/net/bridge/bridge-nf-call-ip6tables

初始化

kubeadm init --kubernetes-version=1.18.3  --apiserver-advertise-address=192.168.88.138   --image-repository registry.aliyuncs.com/google_containers  --service-cidr=10.10.0.0/16 --pod-network-cidr=10.122.0.0/16 --ignore-preflight-errors=Swap

pod-network-cidr参数的为pod网段:,apiserver-advertise-address参数为本机IP。

  如果中途执行有异常可以通过 kubeadm reset 后重新init。
初始化成功执行下面指令
 mkdir -p $HOME/.kube
 sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
 sudo chown $(id -u):$(id -g) $HOME/.kube/config
查看node和pod信息
kubectl get node
kubectl get pod --all-namespaces

安装flannel组件

在master节点(server-a)安装flannel组件

找个梯子下载kube-flannel.yml文件

https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml

下载不了也没关系,我复制给到大家:

---
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: psp.flannel.unprivileged
  annotations:
    seccomp.security.alpha.kubernetes.io/allowedProfileNames: docker/default
    seccomp.security.alpha.kubernetes.io/defaultProfileName: docker/default
    apparmor.security.beta.kubernetes.io/allowedProfileNames: runtime/default
    apparmor.security.beta.kubernetes.io/defaultProfileName: runtime/default
spec:
  privileged: false
  volumes:
    - configMap
    - secret
    - emptyDir
    - hostPath
  allowedHostPaths:
    - pathPrefix: "/etc/cni/net.d"
    - pathPrefix: "/etc/kube-flannel"
    - pathPrefix: "/run/flannel"
  readOnlyRootFilesystem: false
  # Users and groups
  runAsUser:
    rule: RunAsAny
  supplementalGroups:
    rule: RunAsAny
  fsGroup:
    rule: RunAsAny
  # Privilege Escalation
  allowPrivilegeEscalation: false
  defaultAllowPrivilegeEscalation: false
  # Capabilities
  allowedCapabilities: ['NET_ADMIN', 'NET_RAW']
  defaultAddCapabilities: []
  requiredDropCapabilities: []
  # Host namespaces
  hostPID: false
  hostIPC: false
  hostNetwork: true
  hostPorts:
  - min: 0
    max: 65535
  # SELinux
  seLinux:
    # SELinux is unused in CaaSP
    rule: 'RunAsAny'
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: flannel
rules:
  - apiGroups: ['extensions']
    resources: ['podsecuritypolicies']
    verbs: ['use']
    resourceNames: ['psp.flannel.unprivileged']
  - apiGroups:
      - ""
    resources:
      - pods
    verbs:
      - get
  - apiGroups:
      - ""
    resources:
      - nodes
    verbs:
      - list
      - watch
  - apiGroups:
      - ""
    resources:
      - nodes/status
    verbs:
      - patch
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: flannel
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: flannel
subjects:
- kind: ServiceAccount
  name: flannel
  namespace: kube-system
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: flannel
  namespace: kube-system
---
kind: ConfigMap
apiVersion: v1
metadata:
  name: kube-flannel-cfg
  namespace: kube-system
  labels:
    tier: node
    app: flannel
data:
  cni-conf.json: |
    {
      "name": "cbr0",
      "cniVersion": "0.3.1",
      "plugins": [
        {
          "type": "flannel",
          "delegate": {
            "hairpinMode": true,
            "isDefaultGateway": true
          }
        },
        {
          "type": "portmap",
          "capabilities": {
            "portMappings": true
          }
        }
      ]
    }
  net-conf.json: |
    {
      "Network": "10.244.0.0/16",
      "Backend": {
        "Type": "vxlan"
      }
    }
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: kube-flannel-ds-amd64
  namespace: kube-system
  labels:
    tier: node
    app: flannel
spec:
  selector:
    matchLabels:
      app: flannel
  template:
    metadata:
      labels:
        tier: node
        app: flannel
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: kubernetes.io/os
                    operator: In
                    values:
                      - linux
                  - key: kubernetes.io/arch
                    operator: In
                    values:
                      - amd64
      hostNetwork: true
      priorityClassName: system-node-critical
      tolerations:
      - operator: Exists
        effect: NoSchedule
      serviceAccountName: flannel
      initContainers:
      - name: install-cni
        image: quay.io/coreos/flannel:v0.12.0-amd64
        command:
        - cp
        args:
        - -f
        - /etc/kube-flannel/cni-conf.json
        - /etc/cni/net.d/10-flannel.conflist
        volumeMounts:
        - name: cni
          mountPath: /etc/cni/net.d
        - name: flannel-cfg
          mountPath: /etc/kube-flannel/
      containers:
      - name: kube-flannel
        image: quay.io/coreos/flannel:v0.12.0-amd64
        command:
        - /opt/bin/flanneld
        args:
        - --ip-masq
        - --kube-subnet-mgr
        resources:
          requests:
            cpu: "100m"
            memory: "50Mi"
          limits:
            cpu: "100m"
            memory: "50Mi"
        securityContext:
          privileged: false
          capabilities:
            add: ["NET_ADMIN", "NET_RAW"]
        env:
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        volumeMounts:
        - name: run
          mountPath: /run/flannel
        - name: flannel-cfg
          mountPath: /etc/kube-flannel/
      volumes:
        - name: run
          hostPath:
            path: /run/flannel
        - name: cni
          hostPath:
            path: /etc/cni/net.d
        - name: flannel-cfg
          configMap:
            name: kube-flannel-cfg
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: kube-flannel-ds-arm64
  namespace: kube-system
  labels:
    tier: node
    app: flannel
spec:
  selector:
    matchLabels:
      app: flannel
  template:
    metadata:
      labels:
        tier: node
        app: flannel
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: kubernetes.io/os
                    operator: In
                    values:
                      - linux
                  - key: kubernetes.io/arch
                    operator: In
                    values:
                      - arm64
      hostNetwork: true
      priorityClassName: system-node-critical
      tolerations:
      - operator: Exists
        effect: NoSchedule
      serviceAccountName: flannel
      initContainers:
      - name: install-cni
        image: quay.io/coreos/flannel:v0.12.0-arm64
        command:
        - cp
        args:
        - -f
        - /etc/kube-flannel/cni-conf.json
        - /etc/cni/net.d/10-flannel.conflist
        volumeMounts:
        - name: cni
          mountPath: /etc/cni/net.d
        - name: flannel-cfg
          mountPath: /etc/kube-flannel/
      containers:
      - name: kube-flannel
        image: quay.io/coreos/flannel:v0.12.0-arm64
        command:
        - /opt/bin/flanneld
        args:
        - --ip-masq
        - --kube-subnet-mgr
        resources:
          requests:
            cpu: "100m"
            memory: "50Mi"
          limits:
            cpu: "100m"
            memory: "50Mi"
        securityContext:
          privileged: false
          capabilities:
             add: ["NET_ADMIN", "NET_RAW"]
        env:
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        volumeMounts:
        - name: run
          mountPath: /run/flannel
        - name: flannel-cfg
          mountPath: /etc/kube-flannel/
      volumes:
        - name: run
          hostPath:
            path: /run/flannel
        - name: cni
          hostPath:
            path: /etc/cni/net.d
        - name: flannel-cfg
          configMap:
            name: kube-flannel-cfg
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: kube-flannel-ds-arm
  namespace: kube-system
  labels:
    tier: node
    app: flannel
spec:
  selector:
    matchLabels:
      app: flannel
  template:
    metadata:
      labels:
        tier: node
        app: flannel
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: kubernetes.io/os
                    operator: In
                    values:
                      - linux
                  - key: kubernetes.io/arch
                    operator: In
                    values:
                      - arm
      hostNetwork: true
      priorityClassName: system-node-critical
      tolerations:
      - operator: Exists
        effect: NoSchedule
      serviceAccountName: flannel
      initContainers:
      - name: install-cni
        image: quay.io/coreos/flannel:v0.12.0-arm
        command:
        - cp
        args:
        - -f
        - /etc/kube-flannel/cni-conf.json
        - /etc/cni/net.d/10-flannel.conflist
        volumeMounts:
        - name: cni
          mountPath: /etc/cni/net.d
        - name: flannel-cfg
          mountPath: /etc/kube-flannel/
      containers:
      - name: kube-flannel
        image: quay.io/coreos/flannel:v0.12.0-arm
        command:
        - /opt/bin/flanneld
        args:
        - --ip-masq
        - --kube-subnet-mgr
        resources:
          requests:
            cpu: "100m"
            memory: "50Mi"
          limits:
            cpu: "100m"
            memory: "50Mi"
        securityContext:
          privileged: false
          capabilities:
             add: ["NET_ADMIN", "NET_RAW"]
        env:
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        volumeMounts:
        - name: run
          mountPath: /run/flannel
        - name: flannel-cfg
          mountPath: /etc/kube-flannel/
      volumes:
        - name: run
          hostPath:
            path: /run/flannel
        - name: cni
          hostPath:
            path: /etc/cni/net.d
        - name: flannel-cfg
          configMap:
            name: kube-flannel-cfg
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: kube-flannel-ds-ppc64le
  namespace: kube-system
  labels:
    tier: node
    app: flannel
spec:
  selector:
    matchLabels:
      app: flannel
  template:
    metadata:
      labels:
        tier: node
        app: flannel
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: kubernetes.io/os
                    operator: In
                    values:
                      - linux
                  - key: kubernetes.io/arch
                    operator: In
                    values:
                      - ppc64le
      hostNetwork: true
      priorityClassName: system-node-critical
      tolerations:
      - operator: Exists
        effect: NoSchedule
      serviceAccountName: flannel
      initContainers:
      - name: install-cni
        image: quay.io/coreos/flannel:v0.12.0-ppc64le
        command:
        - cp
        args:
        - -f
        - /etc/kube-flannel/cni-conf.json
        - /etc/cni/net.d/10-flannel.conflist
        volumeMounts:
        - name: cni
          mountPath: /etc/cni/net.d
        - name: flannel-cfg
          mountPath: /etc/kube-flannel/
      containers:
      - name: kube-flannel
        image: quay.io/coreos/flannel:v0.12.0-ppc64le
        command:
        - /opt/bin/flanneld
        args:
        - --ip-masq
        - --kube-subnet-mgr
        resources:
          requests:
            cpu: "100m"
            memory: "50Mi"
          limits:
            cpu: "100m"
            memory: "50Mi"
        securityContext:
          privileged: false
          capabilities:
             add: ["NET_ADMIN", "NET_RAW"]
        env:
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        volumeMounts:
        - name: run
          mountPath: /run/flannel
        - name: flannel-cfg
          mountPath: /etc/kube-flannel/
      volumes:
        - name: run
          hostPath:
            path: /run/flannel
        - name: cni
          hostPath:
            path: /etc/cni/net.d
        - name: flannel-cfg
          configMap:
            name: kube-flannel-cfg
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: kube-flannel-ds-s390x
  namespace: kube-system
  labels:
    tier: node
    app: flannel
spec:
  selector:
    matchLabels:
      app: flannel
  template:
    metadata:
      labels:
        tier: node
        app: flannel
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: kubernetes.io/os
                    operator: In
                    values:
                      - linux
                  - key: kubernetes.io/arch
                    operator: In
                    values:
                      - s390x
      hostNetwork: true
      priorityClassName: system-node-critical
      tolerations:
      - operator: Exists
        effect: NoSchedule
      serviceAccountName: flannel
      initContainers:
      - name: install-cni
        image: quay.io/coreos/flannel:v0.12.0-s390x
        command:
        - cp
        args:
        - -f
        - /etc/kube-flannel/cni-conf.json
        - /etc/cni/net.d/10-flannel.conflist
        volumeMounts:
        - name: cni
          mountPath: /etc/cni/net.d
        - name: flannel-cfg
          mountPath: /etc/kube-flannel/
      containers:
      - name: kube-flannel
        image: quay.io/coreos/flannel:v0.12.0-s390x
        command:
        - /opt/bin/flanneld
        args:
        - --ip-masq
        - --kube-subnet-mgr
        resources:
          requests:
            cpu: "100m"
            memory: "50Mi"
          limits:
            cpu: "100m"
            memory: "50Mi"
        securityContext:
          privileged: false
          capabilities:
             add: ["NET_ADMIN", "NET_RAW"]
        env:
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        volumeMounts:
        - name: run
          mountPath: /run/flannel
        - name: flannel-cfg
          mountPath: /etc/kube-flannel/
      volumes:
        - name: run
          hostPath:
            path: /run/flannel
        - name: cni
          hostPath:
            path: /etc/cni/net.d
        - name: flannel-cfg
          configMap:
            name: kube-flannel-cfg

先拉取依赖镜像

 docker pull  quay.io/coreos/flannel:v0.12.0-amd64

把上面文件保存到服务器然后执行下面命令

kubectl apply -f kube-flannel.yml

安装dashboard

在master节点(server-a)安装dashboard组件

继续用梯子下载recommended.yml文件

https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.3/aio/deploy/recommended.yaml

没梯子的可以复制下方原文件

# Copyright 2017 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

apiVersion: v1
kind: Namespace
metadata:
  name: kubernetes-dashboard

---

apiVersion: v1
kind: ServiceAccount
metadata:
  labels:
    k8s-app: kubernetes-dashboard
  name: kubernetes-dashboard
  namespace: kubernetes-dashboard

---

kind: Service
apiVersion: v1
metadata:
  labels:
    k8s-app: kubernetes-dashboard
  name: kubernetes-dashboard
  namespace: kubernetes-dashboard
spec:
  ports:
    - port: 443
      targetPort: 8443
  selector:
    k8s-app: kubernetes-dashboard

---

apiVersion: v1
kind: Secret
metadata:
  labels:
    k8s-app: kubernetes-dashboard
  name: kubernetes-dashboard-certs
  namespace: kubernetes-dashboard
type: Opaque

---

apiVersion: v1
kind: Secret
metadata:
  labels:
    k8s-app: kubernetes-dashboard
  name: kubernetes-dashboard-csrf
  namespace: kubernetes-dashboard
type: Opaque
data:
  csrf: ""

---

apiVersion: v1
kind: Secret
metadata:
  labels:
    k8s-app: kubernetes-dashboard
  name: kubernetes-dashboard-key-holder
  namespace: kubernetes-dashboard
type: Opaque

---

kind: ConfigMap
apiVersion: v1
metadata:
  labels:
    k8s-app: kubernetes-dashboard
  name: kubernetes-dashboard-settings
  namespace: kubernetes-dashboard

---

kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  labels:
    k8s-app: kubernetes-dashboard
  name: kubernetes-dashboard
  namespace: kubernetes-dashboard
rules:
  # Allow Dashboard to get, update and delete Dashboard exclusive secrets.
  - apiGroups: [""]
    resources: ["secrets"]
    resourceNames: ["kubernetes-dashboard-key-holder", "kubernetes-dashboard-certs", "kubernetes-dashboard-csrf"]
    verbs: ["get", "update", "delete"]
    # Allow Dashboard to get and update 'kubernetes-dashboard-settings' config map.
  - apiGroups: [""]
    resources: ["configmaps"]
    resourceNames: ["kubernetes-dashboard-settings"]
    verbs: ["get", "update"]
    # Allow Dashboard to get metrics.
  - apiGroups: [""]
    resources: ["services"]
    resourceNames: ["heapster", "dashboard-metrics-scraper"]
    verbs: ["proxy"]
  - apiGroups: [""]
    resources: ["services/proxy"]
    resourceNames: ["heapster", "http:heapster:", "https:heapster:", "dashboard-metrics-scraper", "http:dashboard-metrics-scraper"]
    verbs: ["get"]

---

kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  labels:
    k8s-app: kubernetes-dashboard
  name: kubernetes-dashboard
rules:
  # Allow Metrics Scraper to get metrics from the Metrics server
  - apiGroups: ["metrics.k8s.io"]
    resources: ["pods", "nodes"]
    verbs: ["get", "list", "watch"]

---

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  labels:
    k8s-app: kubernetes-dashboard
  name: kubernetes-dashboard
  namespace: kubernetes-dashboard
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: kubernetes-dashboard
subjects:
  - kind: ServiceAccount
    name: kubernetes-dashboard
    namespace: kubernetes-dashboard

---

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: kubernetes-dashboard
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: kubernetes-dashboard
subjects:
  - kind: ServiceAccount
    name: kubernetes-dashboard
    namespace: kubernetes-dashboard

---

kind: Deployment
apiVersion: apps/v1
metadata:
  labels:
    k8s-app: kubernetes-dashboard
  name: kubernetes-dashboard
  namespace: kubernetes-dashboard
spec:
  replicas: 1
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      k8s-app: kubernetes-dashboard
  template:
    metadata:
      labels:
        k8s-app: kubernetes-dashboard
    spec:
      containers:
        - name: kubernetes-dashboard
          image: kubernetesui/dashboard:v2.0.3
          imagePullPolicy: Always
          ports:
            - containerPort: 8443
              protocol: TCP
          args:
            - --auto-generate-certificates
            - --namespace=kubernetes-dashboard
            # Uncomment the following line to manually specify Kubernetes API server Host
            # If not specified, Dashboard will attempt to auto discover the API server and connect
            # to it. Uncomment only if the default does not work.
            # - --apiserver-host=http://my-address:port
          volumeMounts:
            - name: kubernetes-dashboard-certs
              mountPath: /certs
              # Create on-disk volume to store exec logs
            - mountPath: /tmp
              name: tmp-volume
          livenessProbe:
            httpGet:
              scheme: HTTPS
              path: /
              port: 8443
            initialDelaySeconds: 30
            timeoutSeconds: 30
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            runAsUser: 1001
            runAsGroup: 2001
      volumes:
        - name: kubernetes-dashboard-certs
          secret:
            secretName: kubernetes-dashboard-certs
        - name: tmp-volume
          emptyDir: {}
      serviceAccountName: kubernetes-dashboard
      nodeSelector:
        "kubernetes.io/os": linux
      # Comment the following tolerations if Dashboard must not be deployed on master
      tolerations:
        - key: node-role.kubernetes.io/master
          effect: NoSchedule

---

kind: Service
apiVersion: v1
metadata:
  labels:
    k8s-app: dashboard-metrics-scraper
  name: dashboard-metrics-scraper
  namespace: kubernetes-dashboard
spec:
  ports:
    - port: 8000
      targetPort: 8000
  selector:
    k8s-app: dashboard-metrics-scraper

---

kind: Deployment
apiVersion: apps/v1
metadata:
  labels:
    k8s-app: dashboard-metrics-scraper
  name: dashboard-metrics-scraper
  namespace: kubernetes-dashboard
spec:
  replicas: 1
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      k8s-app: dashboard-metrics-scraper
  template:
    metadata:
      labels:
        k8s-app: dashboard-metrics-scraper
      annotations:
        seccomp.security.alpha.kubernetes.io/pod: 'runtime/default'
    spec:
      containers:
        - name: dashboard-metrics-scraper
          image: kubernetesui/metrics-scraper:v1.0.4
          ports:
            - containerPort: 8000
              protocol: TCP
          livenessProbe:
            httpGet:
              scheme: HTTP
              path: /
              port: 8000
            initialDelaySeconds: 30
            timeoutSeconds: 30
          volumeMounts:
          - mountPath: /tmp
            name: tmp-volume
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            runAsUser: 1001
            runAsGroup: 2001
      serviceAccountName: kubernetes-dashboard
      nodeSelector:
        "kubernetes.io/os": linux
      # Comment the following tolerations if Dashboard must not be deployed on master
      tolerations:
        - key: node-role.kubernetes.io/master
          effect: NoSchedule
      volumes:
        - name: tmp-volume
          emptyDir: {}
第39行修改,端口范围30000-32767
spec:
  type: NodePort
  ports:
    - port: 443
      targetPort: 8443
      nodePort: 30221
  selector:
    k8s-app: kubernetes-dashboard

第137行开始,修改账户权限,主要三个参数,kind: ClusterRoleBinding,roleRef-kind: ClusterRole,roleRef-name: cluster-admin

---

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  labels:
    k8s-app: kubernetes-dashboard
  name: kubernetes-dashboard
  namespace: kubernetes-dashboard
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
  - kind: ServiceAccount
    name: kubernetes-dashboard
    namespace: kubernetes-dashboard

---

保存到服务器后执行以下命令

kubectl apply -f recommended.yaml

等待一段时间启动成功后,https://ip+nodePort,查看UI

Token通过下面指令获取

kubectl -n kubernetes-dashboard get secret

kubectl describe secrets -n kubernetes-dashboard kubernetes-dashboard-token-kfcp2  | grep token | awk 'NR==3{print $2}'

加入Worker节点

在server-b与server-c执行下面操作

把上面init后的那句join拷贝过来,如果忘记了可以在master节点执行下面指令:

kubeadm token list

openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2>/dev/null | openssl dgst -sha256 -hex | sed 's/^.* //'

通过返回的数据拼装成下面指令

kubeadm join 192.168.88.138:6443 --token 2zebwy.1549suwrkkven7ow  --discovery-token-ca-cert-hash sha256:c61af74d6e4ba1871eceaef4e769d14a20a86c9276ac0899f8ec6b08b89f532b

查看节点信息

kubectl get node

部署Web应用

在master节点(sever-a)执行下面操作

部署应用前建议有需要的朋友到【.Net微服务实战之CI/CD】看看如何搭建docker私有仓库,后面需要用到,搭建后私有库后执行下面指令

kubectl create secret docker-registry docker-registry-secret --docker-server=192.168.88.141:6000 --docker-username=admin --docker-password=123456789

docker-server就是docker私有仓库的地址

下面是yaml模板,注意imagePullSecrets-name与上面的命名的一致,其余的可以查看yaml里的注释

apiVersion: apps/v1
kind: Deployment # Deployment为多个Pod副本
metadata:
  name: testdockerswarm-deployment
  labels:
    app: testdockerswarm-deployment
spec:
  replicas: 2 # 实例数量
  selector:
    matchLabels: # 定义该部署匹配哪些Pod
      app: testdockerswarm
  minReadySeconds: 3 # 可选,指定Pod可以变成可用状态的最小秒数,默认是0
  strategy:
    type: RollingUpdate # 部署策略类型,使用RollingUpdate可以保证部署期间服务不间断
    rollingUpdate:
      maxUnavailable: 1 # 部署时最大允许停止的Pod数量
      maxSurge: 1 # 部署时最大允许创建的Pod数量
  template: # 用来指定Pod的模板,与Pod的定义类似
    metadata:
      labels: # Pod标签,与上面matchLabels对应
        app: testdockerswarm
    spec:
      imagePullSecrets:
        - name: docker-registry-secret
      containers:
        - name: testdockerswarm
          image: 192.168.88.141:6000/testdockerswarm
          imagePullPolicy: Always # Always每次拉去新镜像
          ports:
            - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: testdockerswarm-service
  labels:  
    name: testdockerswarm-service
spec:
  selector:
    app: testdockerswarm #与template-labels参数pod标签一致
  ports:
    - protocol: TCP
      port: 80 #clusterIP开放的端口
      targetPort: 80 #container开放的端口,与containerPort一致
      nodePort: 31221 # 所有的节点都会开放此端口,此端口供外部调用。
  type: NodePort

把yaml文件保存到服务器后执行下面命令

kubectl create -f testdockerswarm.yml

整个搭建部署的过程基本上到这里结束了。

访问

可以通过指令kubectl get service得到ClusterIP,分别在server-c和sever-b执行curl 10.10.184.184

也可以通过执行kubectl get pods -o wide得到pod ip,在server-c执行curl 10.122.2.5 和 server-b执行curl 10.122.1.7

也可以在外部访问 server-c和server-b的 ip + 31221

如果节点有异常可以通过下面指令排查

journalctl -f -u kubelet.service | grep -i error -C 500

如果Pod无法正常running可以通过下面指令查看

kubectl describe pod testdockerswarm-deployment-7bc647d87d-qwvzm