如何使用Terraform和Kubernetes在亚马逊EKS上部署Java微服务

637 阅读13分钟

谈到基础设施,公有云是最近最流行的选择,特别是亚马逊网络服务(AWS)。如果你在那些幸运或不幸运(取决于你如何看待它)的团队中运行微服务,那么你需要一种方法来协调他们的部署。当涉及到协调微服务时,Kubernetes是事实上的选择。大多数公有云供应商也提供管理的Kubernetes服务;例如,谷歌提供谷歌Kubernetes引擎(GKE),微软提供Azure Kubernetes服务(AKS),亚马逊提供亚马逊Elastic Kubernetes服务(EKS)。

这并不意味着在公有云上部署和管理微服务是容易的;每一种云服务都有自己的挑战和痛苦。对于亚马逊EKS来说尤其如此,在我看来,它是最难使用的Kubernetes服务,但也是最灵活的一个。这是因为EKS由一些聪明的协调程序组成,在其他AWS服务(如EC2、EBS等)之上跳着复杂的舞蹈。

如果你想在EKS上运行一个微服务堆栈,你将需要花费一些额外的时间和精力来设置和管理它。这就是Terraform等基础设施即代码(IaC)工具发挥作用的地方。

以下是你今天要学习做的事情:

  • 使用JHipster、Spring Boot和Spring Cloud搭建一个Java微服务栈的支架
  • 在AWS上使用Terraform创建EKS集群、虚拟私有云(VPC)、子网和所需的Kubernetes附加组件
  • 使用Okta为微服务栈设置OIDC认证
  • 构建并部署微服务栈到集群上

前提条件

目录

为什么是Terraform,为什么不是CloudFormation?

在这一点上,你脑海中出现的第一个问题可能是:"为什么不使用CloudFormation?"。这是一个很好的问题,毕竟CloudFormation是由AWS建立的,因此听起来是一个管理AWS资源的优秀解决方案。但是,任何尝试过CloudFormation和Terraform的人可能会告诉你,忘记CloudFormation的存在。我认为CloudFormation要比Terraform复杂得多,对开发者也不太友好。你还需要用YAML或JSON在CloudFormation中编写更多的模板。Yikes!最重要的是,Terraform要比CloudFormation强大和灵活得多。它是跨平台的,这意味着你可以用一个工具照顾到任何平台上的所有基础设施管理需求。

使用JHipster搭建一个Java微服务栈

你需要一个微服务栈来部署到集群上。我使用的是一个微服务堆栈,为演示目的,使用JHipster搭建了支架。如果你愿意,你可以使用另一个微服务栈。如果你喜欢使用与本演示相同的应用程序,那么你可以使用JHipsterJDL来搭建它,或者从GitHub克隆样本库。

JHipster microservice architecture

选项1:使用JHipster搭建微服务栈的脚手架

mkdir jhipster-microservice-stack
cd jhipster-microservice-stack
# download the JDL file
jhipster download https://raw.githubusercontent.com/oktadev/okta-jhipster-k8s-eks-microservices-example/main/apps.jdl
# scaffold
jhipster jdl apps.jdl

选项2:克隆样本库

git clone https://github.com/oktadev/okta-jhipster-k8s-eks-microservices-example

JHipster脚手架的示例应用程序有一个网关应用程序,两个微服务,并使用JHipster注册表进行服务发现和集中配置。

使用Terraform创建一个EKS集群

现在让我们进入本教程的重要部分。在AWS中创建一个EKS集群并不像在谷歌云平台(GCP)中那么简单。你还需要创建更多的资源,以便一切都能正常工作,不会出现意外。你将使用一堆Terraform提供者来帮助完成这个任务,你还将使用一些预建的Terraform模块,如AWS VPC Terraform模块Amazon EKS Blueprints for Terraform,以减少你需要编写的模板数量。

这些是你将创建的AWS资源和VPC架构。

AWS EKS and VPC architecture

建立Terraform配置

首先,确保你使用特定版本的提供者,因为不同版本可能使用不同的属性和功能。创建一个versions.tf 文件:

mkdir terraform
cd terraform
touch versions.tf

在该文件中添加以下内容:

terraform {
  required_version = ">= 1.0.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 3.72"
    }
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = ">= 2.10"
    }
    helm = {
      source  = "hashicorp/helm"
      version = ">= 2.4.1"
    }
    random = {
      source  = "hashicorp/random"
      version = ">= 3.2.0"
    }
    nullres = {
      source  = "hashicorp/null"
      version = ">= 3.1"
    }
  }
}

接下来,你需要定义变量并配置提供者。创建一个config.tf 文件:

touch config.tf

在文件中添加以下内容:

# ##  To save state in s3. Update to suit your needs
# backend "s3" {
#   bucket = "create-an-s3-bucket-and-provide-name-here"
#   region = local.region
#   key    = "eks-cluster-with-new-vpc/terraform.tfstate"
# }

variable "region" {
  default     = "eu-west-1"
  description = "AWS region"
}

resource "random_string" "suffix" {
  length  = 8
  special = false
}

data "aws_availability_zones" "available" {}

locals {
  name            = "okta-jhipster-eks-${random_string.suffix.result}"
  region          = var.region
  cluster_version = "1.22"
  instance_types  = ["t2.large"] # can be multiple, comma separated

  vpc_cidr = "10.0.0.0/16"
  azs      = slice(data.aws_availability_zones.available.names, 0, 3)

  tags = {
    Blueprint  = local.name
    GitHubRepo = "github.com/aws-ia/terraform-aws-eks-blueprints"
  }
}

provider "aws" {
  region = local.region
}

# Kubernetes provider
# You should **not** schedule deployments and services in this workspace.
# This keeps workspaces modular (one for provision EKS, another for scheduling
# Kubernetes resources) as per best practices.
provider "kubernetes" {
  host                   = module.eks_blueprints.eks_cluster_endpoint
  cluster_ca_certificate = base64decode(module.eks_blueprints.eks_cluster_certificate_authority_data)

  exec {
    api_version = "client.authentication.k8s.io/v1alpha1"
    command     = "aws"
    # This requires the awscli to be installed locally where Terraform is executed
    args = ["eks", "get-token", "--cluster-name", module.eks_blueprints.eks_cluster_id]
  }
}

provider "helm" {
  kubernetes {
    host                   = module.eks_blueprints.eks_cluster_endpoint
    cluster_ca_certificate = base64decode(module.eks_blueprints.eks_cluster_certificate_authority_data)

    exec {
      api_version = "client.authentication.k8s.io/v1alpha1"
      command     = "aws"
      # This requires the awscli to be installed locally where Terraform is executed
      args = ["eks", "get-token", "--cluster-name", module.eks_blueprints.eks_cluster_id]
    }
  }
}

你可以取消上面的backend 部分,将状态保存在S3而不是你的本地文件系统。这被推荐用于生产设置,以便团队中的每个人都有相同的状态。这个文件定义了整个工作区使用的可配置变量和本地变量,并配置了一些使用的提供者。Kubernetes提供者包含在这个文件中,这样EKS模块才能成功完成。否则,它在创建kubernetes_config_map.aws_auth 时会抛出一个错误。helm提供者用于向集群安装Kubernetes附加组件。

构建VPC

接下来,你需要一个VPC、子网、路由表和其他网络位。你将使用vpc ,该模块来自 terraform-aws-modules仓库的模块。这个模块是AWS VPC模块的一个封装器。它使配置VPC和所有其他所需的网络资源变得更容易。创建一个vpc.tf 文件:

touch vpc.tf

在文件中添加以下内容:

#---------------------------------------------------------------
# VPC, Subnets, Internet gateway, Route tables, etc.
#---------------------------------------------------------------
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 3.0"

  name = local.name
  cidr = local.vpc_cidr

  azs             = local.azs
  public_subnets  = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k)]
  private_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 10)]

  enable_nat_gateway   = true
  single_nat_gateway   = true
  enable_dns_hostnames = true

  # Manage so we can name
  manage_default_network_acl    = true
  default_network_acl_tags      = { Name = "${local.name}-default" }
  manage_default_route_table    = true
  default_route_table_tags      = { Name = "${local.name}-default" }
  manage_default_security_group = true
  default_security_group_tags   = { Name = "${local.name}-default" }

  public_subnet_tags = {
    "kubernetes.io/cluster/${local.name}" = "shared"
    "kubernetes.io/role/elb"              = 1
  }

  private_subnet_tags = {
    "kubernetes.io/cluster/${local.name}" = "shared"
    "kubernetes.io/role/internal-elb"     = 1
  }

  tags = local.tags
}

这将创建:

  • 一个新的VPC,三个私有子网,和三个公共子网。
  • 公共子网的互联网网关和NAT网关。
  • 网关的AWS路由,公共/私人路由表,以及路由表关联。

建立EKS集群

现在你已经完成了网络部分,你可以为EKS集群和它的附加组件建立配置。你将使用eks_blueprints 模块。 terraform-aws-eks-blueprints的模块,它是一个围绕 terraform-aws-modules并提供额外的模块来配置EKS的附加组件。

创建一个eks-cluster.tf 文件:

touch eks-cluster.tf

在文件中添加以下内容:

#---------------------------------------------------------------
# EKS cluster, worker nodes, security groups, IAM roles, K8s add-ons, etc.
#---------------------------------------------------------------
module "eks_blueprints" {
  source = "github.com/aws-ia/terraform-aws-eks-blueprints?ref=v4.0.9"

  cluster_name    = local.name
  cluster_version = local.cluster_version

  vpc_id             = module.vpc.vpc_id
  private_subnet_ids = module.vpc.private_subnets

  managed_node_groups = {
    node = {
      node_group_name = "managed-ondemand"
      instance_types  = local.instance_types
      min_size        = 2
      subnet_ids      = module.vpc.private_subnets
    }
  }

  tags = local.tags
}

module "eks_blueprints_kubernetes_addons" {
  source = "github.com/aws-ia/terraform-aws-eks-blueprints//modules/kubernetes-addons?ref=v4.0.9"

  eks_cluster_id       = module.eks_blueprints.eks_cluster_id
  eks_cluster_endpoint = module.eks_blueprints.eks_cluster_endpoint
  eks_oidc_provider    = module.eks_blueprints.oidc_provider
  eks_cluster_version  = module.eks_blueprints.eks_cluster_version

  # EKS Managed Add-ons
  enable_amazon_eks_vpc_cni    = true
  enable_amazon_eks_coredns    = true
  enable_amazon_eks_kube_proxy = true

  # K8S Add-ons
  enable_aws_load_balancer_controller = true
  enable_metrics_server               = true
  enable_cluster_autoscaler           = true
  enable_aws_cloudwatch_metrics       = false

  tags = local.tags

}

# To update local kubeconfig with new cluster details
resource "null_resource" "kubeconfig" {
  depends_on = [module.eks_blueprints_kubernetes_addons]
  provisioner "local-exec" {
    command = "aws eks --region ${local.region}  update-kubeconfig --name $AWS_CLUSTER_NAME"
    environment = {
      AWS_CLUSTER_NAME = local.name
    }
  }
}

eks_blueprints 模块定义创建:

  • EKS集群控制平面,有一个管理的节点组和fargate配置文件。
  • 集群和节点安全组和规则,需要的IAM角色和策略。
  • 和AWS密钥管理服务(KMS)配置。

eks_blueprints_kubernetes_addons 模块定义创建:

  • 亚马逊EKS附加组件vpc-cni、CoreDNS和kube-proxy。
  • AWS负载平衡器控制器,提供AWS网络负载平衡器以分配流量。
  • 指标服务器,以及用于扩展工作负载的群集自动调节器。

null_resource 配置用新的集群细节更新你的本地kubeconfig。这不是配置的一个必要步骤,只是一个方便的黑客。

最后,你还可以定义一些要捕获的输出。创建一个outputs.tf 文件:

touch outputs.tf

在该文件中添加以下内容:

output "vpc_private_subnet_cidr" {
  description = "VPC private subnet CIDR"
  value       = module.vpc.private_subnets_cidr_blocks
}

output "vpc_public_subnet_cidr" {
  description = "VPC public subnet CIDR"
  value       = module.vpc.public_subnets_cidr_blocks
}

output "vpc_cidr" {
  description = "VPC CIDR"
  value       = module.vpc.vpc_cidr_block
}

output "eks_cluster_id" {
  description = "EKS cluster ID"
  value       = module.eks_blueprints.eks_cluster_id
}

output "eks_managed_nodegroups" {
  description = "EKS managed node groups"
  value       = module.eks_blueprints.managed_node_groups
}

output "eks_managed_nodegroup_ids" {
  description = "EKS managed node group ids"
  value       = module.eks_blueprints.managed_node_groups_id
}

output "eks_managed_nodegroup_arns" {
  description = "EKS managed node group arns"
  value       = module.eks_blueprints.managed_node_group_arn
}

output "eks_managed_nodegroup_role_name" {
  description = "EKS managed node group role name"
  value       = module.eks_blueprints.managed_node_group_iam_role_names
}

output "eks_managed_nodegroup_status" {
  description = "EKS managed node group status"
  value       = module.eks_blueprints.managed_node_groups_status
}

output "configure_kubectl" {
  description = "Configure kubectl: make sure you're logged in with the correct AWS profile and run the following command to update your kubeconfig"
  value       = module.eks_blueprints.configure_kubectl
}

配置集群

我们的Terraform定义已经准备好了。现在你可以配置集群了。首先,初始化Terraform工作区,并计划更改。

# download modules and providers. Initialize state.
terraform init
# see a preview of what will be done
terraform plan

审查计划,确保一切正确。确保你已经配置了AWS CLI和IAM Authenticator,以使用正确的AWS账户。如果没有,请运行以下程序:

# Visit https://console.aws.amazon.com/iam/home?#/security_credentials for creating access keys
aws configure

现在你可以应用这些变化:

terraform apply

在出现提示时,输入yes ,以确认。这将需要一段时间(15-20分钟),所以请坐下来喝杯咖啡,或者思考一下是什么让你走到了人生的这一步。 😉

一旦EKS集群准备就绪,你将看到输出变量打印到控制台:

configure_kubectl = "aws eks --region eu-west-1 update-kubeconfig --name okta-tf-demo"
eks_cluster_id = "okta-tf-demo"
eks_managed_nodegroup_arns = tolist([
  "arn:aws:eks:eu-west-1:216713166862:nodegroup/okta-tf-demo/managed-ondemand-20220610125341399700000010/f0c0a6d6-b8e1-cf91-3d21-522552d6bc2e",
])
eks_managed_nodegroup_ids = tolist([
  "okta-tf-demo:managed-ondemand-20220610125341399700000010",
])
eks_managed_nodegroup_role_name = tolist([
  "okta-tf-demo-managed-ondemand",
])
eks_managed_nodegroup_status = tolist([
  "ACTIVE",
])
eks_managed_nodegroups = tolist([
  {
    "node" = {
      "managed_nodegroup_arn" = [
        "arn:aws:eks:eu-west-1:216713166862:nodegroup/okta-tf-demo/managed-ondemand-20220610125341399700000010/f0c0a6d6-b8e1-cf91-3d21-522552d6bc2e",
      ]
      "managed_nodegroup_iam_instance_profile_arn" = [
        "arn:aws:iam::216713166862:instance-profile/okta-tf-demo-managed-ondemand",
      ]
      "managed_nodegroup_iam_instance_profile_id" = [
        "okta-tf-demo-managed-ondemand",
      ]
      "managed_nodegroup_iam_role_arn" = [
        "arn:aws:iam::216713166862:role/okta-tf-demo-managed-ondemand",
      ]
      "managed_nodegroup_iam_role_name" = [
        "okta-tf-demo-managed-ondemand",
      ]
      "managed_nodegroup_id" = [
        "okta-tf-demo:managed-ondemand-20220610125341399700000010",
      ]
      "managed_nodegroup_launch_template_arn" = []
      "managed_nodegroup_launch_template_id" = []
      "managed_nodegroup_launch_template_latest_version" = []
      "managed_nodegroup_status" = [
        "ACTIVE",
      ]
    }
  },
])
region = "eu-west-1"
vpc_cidr = "10.0.0.0/16"
vpc_private_subnet_cidr = [
  "10.0.10.0/24",
  "10.0.11.0/24",
  "10.0.12.0/24",
]
vpc_public_subnet_cidr = [
  "10.0.0.0/24",
  "10.0.1.0/24",
  "10.0.2.0/24",
]

如果你运行kdashkubectl get nodes 命令,你应该看到集群的详细信息。

EKS cluster in KDash

使用Okta设置OIDC认证

你可以继续部署示例应用程序。如果您使用的是不使用Okta或OIDC进行认证的样本,您可以跳过这一步。

首先,导航到商店应用程序文件夹。

在你开始之前,你需要一个免费的Okta开发者账户。安装Okta CLI并运行okta register ,以注册一个新账户。如果你已经有一个账户,运行okta login 。然后,运行okta apps create jhipster 。选择默认的应用程序名称,或根据您的需要进行更改。 接受为您提供的默认重定向URI值。

Okta CLI是做什么的?

Okta CLI简化了对JHipster应用程序的配置,并为您做了几件事:

  1. 创建一个具有正确重定向URI的OIDC应用程序。
    • 登录:http://localhost:8080/login/oauth2/code/oidchttp://localhost:8761/login/oauth2/code/oidc
    • 注销:http://localhost:8080http://localhost:8761
  2. 创建JHipster期望的ROLE_ADMINROLE_USER
  3. 将你的当前用户添加到ROLE_ADMINROLE_USER 组中。
  4. 在你的默认授权服务器中创建一个groups ,并将用户的组添加到其中。

注意http://localhost:8761* 重定向URI是为JHipster注册处准备的,在用JHipster创建微服务时经常使用。Okta CLI默认会添加这些。

完成后,你会看到如下的输出:

Okta application configuration has been written to: /path/to/app/.okta.env

运行cat .okta.env (或Windows上的type .okta.env ),查看你的应用程序的发行者和凭证。它看起来会像这样(除了占位符的值会被填充)。

export SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_OIDC_ISSUER_URI="/oauth2/default"
export SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_ID="{clientId}"
export SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_SECRET="{clientSecret}"

注意:您也可以使用Okta管理控制台来创建您的应用程序。参见在Okta上创建一个JHipster应用程序以获得更多信息。

注意:确保将新创建的.okta.env 文件添加到你的.gitignore 文件中,这样你就不会意外地将你的凭证暴露给公众。

.okta.env 文件中的OIDC配置更新kubernetes/registry-k8s/application-configmap.yml 。Spring Cloud Config服务器会从这个文件中读取,并与网关和微服务共享这些值。

data:
  application.yml: |-
    ...
    spring:
      security:
        oauth2:
          client:
            provider:
              oidc:
                issuer-uri: https://<your-okta-domain>/oauth2/default
            registration:
              oidc:
                client-id: <client-id>
                client-secret: <client-secret>

接下来,配置JHipster注册表以使用OIDC进行认证。修改kubernetes/registry-k8s/jhipster-registry.yml ,以启用oauth2 配置文件,该文件已在JHipster注册表应用程序中预先配置。

- name: SPRING_PROFILES_ACTIVE
  value: prod,k8s,oauth2

现在应用程序已经准备好了。

安全的秘密

如果你已经注意到了,你在application-configmap.yml 文件上以纯文本形式设置秘密,这并不理想,也不是安全的最佳做法。对于特定的JHipster应用程序,你可以使用JHipster注册表提供的加密功能对秘密进行加密。请参阅使用Spring Cloud Config加密你的秘密,以了解如何做到这一点。但这也将依赖于作为Kubernetes秘密添加的base64编码的加密密钥,它仍然可以被解密。最好的方法是使用AWS Secrets ManagerHashiCorp Vault等外部服务或Sealed Secrets。要了解这些方法的更多信息,请参见《加密你的Kubernetes秘密》。

部署微服务栈

你已经准备好部署到我们闪亮的新EKS集群,但首先,你需要构建Docker镜像并将其推送到一个容器注册中心。你可以使用亚马逊弹性容器注册中心(ECR)或任何其他容器注册中心。

构建Docker镜像

你需要为每个应用程序构建Docker镜像。这是针对本教程中使用的JHipster应用程序的。导航到每个应用程序文件夹**(商店**、发票产品)并运行以下命令。

./gradlew bootJar -Pprod jib -Djib.to.image=<docker-repo-uri-or-name>/<image-name>

镜像名称应该是:store,invoice, 和product

将应用程序部署到EKS

使用JHipster提供的方便的脚本开始部署。你也可以使用kubectl apply -f <file> 命令手动应用部署。

cd kubernetes
./kubectl-apply.sh -f

EKS cluster in KDash

你也可以运行下面的命令来查看部署的状态。

kubectl get pods -n jhipster

使用端口转发查看注册表,如下所示,你将能够访问应用程序,http://localhost:8761

kubectl port-forward svc/jhipster-registry -n jhipster 8761

你可以使用端口转发访问网关应用程序,如下所示,你将能够访问应用程序:http://localhost:8080

kubectl port-forward svc/store -n jhipster 8080

或者,你可以通过暴露的负载平衡器访问该应用程序。通过导航到KDash中的服务选项卡或运行以下程序,找到store 服务的外部IP。

kubectl get svc store -n jhipster

导航到Okta管理控制台,从左侧导航进入应用程序>应用程序。找到你之前用okta apps create jhipster 创建的应用程序,将kubectl get svc 命令中的外部IP添加到签入重定向URI签出重定向URI中。确保使用与当前localhost 条目相同的路径。

现在你应该能够访问8080端口的store 服务的外部IP,并看到应用程序,而且你应该能够使用Okta凭证登录。

用Terraform拆解集群

完成教程后,你可以通过运行以下命令删除集群和所有用Terraform创建的资源。

cd terraform
# The commands below might take a while to finish.
terraform destroy -target="module.eks_blueprints_kubernetes_addons" -auto-approve
# If deleting VPC fails, then manually delete the load balancers and security groups 
# for the load balancer associated with the VPC from AWS EC2 console and try again.
terraform destroy -target="module.eks_blueprints" -auto-approve
terraform destroy -target="module.vpc" -auto-approve
# cleanup anything left over.
terraform destroy -auto-approve