Skip to content

AWS EKS support #39

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/all/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
aws_batch_fargate "github.com/nullstone-io/deployment-sdk/app/container/aws-batch-fargate"
"github.com/nullstone-io/deployment-sdk/app/container/aws-ecs-ec2"
"github.com/nullstone-io/deployment-sdk/app/container/aws-ecs-fargate"
aws_eks_service "github.com/nullstone-io/deployment-sdk/app/container/aws-eks-service"
gcp_gke_service "github.com/nullstone-io/deployment-sdk/app/container/gcp-gke-service"
"github.com/nullstone-io/deployment-sdk/app/server/aws-beanstalk"
"github.com/nullstone-io/deployment-sdk/app/serverless/aws-lambda-container"
Expand All @@ -18,6 +19,7 @@ var (
aws_batch_fargate.ModuleContractName: aws_batch_fargate.Provider,
aws_ecs_fargate.ModuleContractName: aws_ecs_fargate.Provider,
aws_ecs_ec2.ModuleContractName: aws_ecs_ec2.Provider,
aws_eks_service.ModuleContractName: aws_eks_service.Provider,
aws_s3.ModuleContractName: aws_s3.Provider,
aws_lambda_zip.ModuleContractName: aws_lambda_zip.Provider,
aws_lambda_container.ModuleContractName: aws_lambda_container.Provider,
Expand Down
25 changes: 25 additions & 0 deletions app/container/aws-eks-service/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package aws_eks_service

import (
"github.com/nullstone-io/deployment-sdk/app"
"github.com/nullstone-io/deployment-sdk/aws/ecr"
"github.com/nullstone-io/deployment-sdk/aws/eks"
"gopkg.in/nullstone-io/go-api-client.v0/types"
)

var ModuleContractName = types.ModuleContractName{
Category: string(types.CategoryApp),
Subcategory: string(types.SubcategoryAppContainer),
Provider: "aws",
Platform: "k8s",
Subplatform: "eks",
}

var Provider = app.Provider{
CanDeployImmediate: false,
NewPusher: ecr.NewPusher,
NewDeployer: eks.NewDeployer,
NewDeployWatcher: eks.NewDeployWatcher,
NewStatuser: nil,
NewLogStreamer: eks.NewLogStreamer,
}
30 changes: 30 additions & 0 deletions aws/eks/create_kube_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package eks

import (
"context"
"fmt"
nsaws "github.com/nullstone-io/deployment-sdk/aws"
"github.com/nullstone-io/deployment-sdk/k8s"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)

func CreateKubeConfig(ctx context.Context, region string, cluster ClusterInfoer, user nsaws.User) (*rest.Config, error) {
configCreator := &k8s.ConfigCreator{
ClusterInfoer: cluster,
AuthInfoer: IamUserAuth{
User: user,
Region: region,
ClusterId: cluster.GetClusterName(),
},
}
return configCreator.Create(ctx)
}

func CreateKubeClient(ctx context.Context, region string, cluster ClusterInfoer, user nsaws.User) (*kubernetes.Clientset, error) {
cfg, err := CreateKubeConfig(ctx, region, cluster, user)
if err != nil {
return nil, fmt.Errorf("error creating kube config: %w", err)
}
return kubernetes.NewForConfig(cfg)
}
27 changes: 27 additions & 0 deletions aws/eks/deploy_watcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package eks

import (
"context"
"github.com/nullstone-io/deployment-sdk/app"
"github.com/nullstone-io/deployment-sdk/k8s"
"github.com/nullstone-io/deployment-sdk/logging"
"github.com/nullstone-io/deployment-sdk/outputs"
"k8s.io/client-go/rest"
)

func NewDeployWatcher(ctx context.Context, osWriters logging.OsWriters, source outputs.RetrieverSource, appDetails app.Details) (app.DeployWatcher, error) {
outs, err := outputs.Retrieve[Outputs](ctx, source, appDetails.Workspace, appDetails.WorkspaceConfig)
if err != nil {
return nil, err
}

return &k8s.DeployWatcher{
OsWriters: osWriters,
Details: appDetails,
AppNamespace: outs.ServiceNamespace,
AppName: outs.ServiceName,
NewConfigFn: func(ctx context.Context) (*rest.Config, error) {
return CreateKubeConfig(ctx, outs.Region, outs.ClusterNamespace, outs.Deployer)
},
}, nil
}
145 changes: 145 additions & 0 deletions aws/eks/deployer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package eks

import (
"context"
"fmt"
"github.com/mitchellh/colorstring"
"github.com/nullstone-io/deployment-sdk/app"
env_vars "github.com/nullstone-io/deployment-sdk/env-vars"
"github.com/nullstone-io/deployment-sdk/k8s"
"github.com/nullstone-io/deployment-sdk/logging"
"github.com/nullstone-io/deployment-sdk/outputs"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

const (
DeployReferenceNoop = "no-updated-revision"
)

func NewDeployer(ctx context.Context, osWriters logging.OsWriters, source outputs.RetrieverSource, appDetails app.Details) (app.Deployer, error) {
outs, err := outputs.Retrieve[Outputs](ctx, source, appDetails.Workspace, appDetails.WorkspaceConfig)
if err != nil {
return nil, err
}

return Deployer{
OsWriters: osWriters,
Details: appDetails,
Infra: outs,
}, nil
}

type Deployer struct {
OsWriters logging.OsWriters
Details app.Details
Infra Outputs
}

func (d Deployer) Print() {
stdout, _ := d.OsWriters.Stdout(), d.OsWriters.Stderr()
colorstring.Fprintln(stdout, "[bold]Retrieved GKE service outputs")
fmt.Fprintf(stdout, " cluster_endpoint: %s\n", d.Infra.ClusterNamespace.ClusterEndpoint)
fmt.Fprintf(stdout, " service_namespace: %s\n", d.Infra.ServiceNamespace)
fmt.Fprintf(stdout, " service_name: %s\n", d.Infra.ServiceName)
fmt.Fprintf(stdout, " image_repo_url: %s\n", d.Infra.ImageRepoUrl)
}

func (d Deployer) Deploy(ctx context.Context, meta app.DeployMetadata) (string, error) {
stdout, _ := d.OsWriters.Stdout(), d.OsWriters.Stderr()
d.Print()

if meta.Version == "" {
return "", fmt.Errorf("no version specified, version is required to deploy")
}

fmt.Fprintln(stdout)
fmt.Fprintf(stdout, "Deploying app %q\n", d.Details.App.Name)
if d.Infra.ServiceName == "" {
if d.Infra.JobDefinitionName == "" {
fmt.Fprintf(stdout, "No service_name or job_definition_name in app module. Skipping update service.\n")
fmt.Fprintf(stdout, "Deployed app %q\n", d.Details.App.Name)
fmt.Fprintln(stdout, "")
return "", nil
}

return d.deployJobTemplate(ctx, meta)
}
return d.deployService(ctx, meta)
}

func (d Deployer) deployService(ctx context.Context, meta app.DeployMetadata) (string, error) {
stdout, _ := d.OsWriters.Stdout(), d.OsWriters.Stderr()

kubeClient, err := CreateKubeClient(ctx, d.Infra.Region, d.Infra.ClusterNamespace, d.Infra.Deployer)
if err != nil {
return "", err
}

deployment, err := kubeClient.AppsV1().Deployments(d.Infra.ServiceNamespace).Get(ctx, d.Infra.ServiceName, metav1.GetOptions{})
if err != nil {
return "", err
}
curGeneration := deployment.Generation

// Update deployment definition
deployment.ObjectMeta = k8s.UpdateVersionLabel(deployment.ObjectMeta, meta.Version)
deployment.Spec.Template, err = d.updatePodTemplate(deployment.Spec.Template, meta)
if err != nil {
return "", err
}

updated, err := kubeClient.AppsV1().Deployments(d.Infra.ServiceNamespace).Update(ctx, deployment, metav1.UpdateOptions{})
if err != nil {
return "", fmt.Errorf("error deploying app: %w", err)
}
updGeneration := updated.Generation
reference := fmt.Sprintf("%d", updGeneration)

if curGeneration == updGeneration {
reference = DeployReferenceNoop
fmt.Fprintln(stdout, "No changes made to deployment.")
} else {
fmt.Fprintf(stdout, "Created new deployment (generation = %s).\n", reference)
}

fmt.Fprintf(stdout, "Deployed app %q\n", d.Details.App.Name)
return reference, nil
}

func (d Deployer) deployJobTemplate(ctx context.Context, meta app.DeployMetadata) (string, error) {
stdout, _ := d.OsWriters.Stdout(), d.OsWriters.Stderr()

kubeClient, err := CreateKubeClient(ctx, d.Infra.Region, d.Infra.ClusterNamespace, d.Infra.Deployer)
if err != nil {
return "", err
}

// Retrieve and update job definition
jobDef, configMap, err := k8s.GetJobDefinition(ctx, kubeClient, d.Infra.ServiceNamespace, d.Infra.JobDefinitionName)
if err != nil {
return "", err
}
jobDef.ObjectMeta = k8s.UpdateVersionLabel(jobDef.ObjectMeta, meta.Version)
jobDef.Spec.Template, err = d.updatePodTemplate(jobDef.Spec.Template, meta)
if err != nil {
return "", fmt.Errorf("cannot find main container %q in spec", d.Infra.MainContainerName)
}
if err := k8s.UpdateJobDefinition(ctx, kubeClient, d.Infra.ServiceNamespace, jobDef, configMap); err != nil {
return "", err
}

fmt.Fprintf(stdout, "Updated job template with new application version (%s) and environment variables\n", meta.Version)
return "", nil
}

func (d Deployer) updatePodTemplate(template corev1.PodTemplateSpec, meta app.DeployMetadata) (corev1.PodTemplateSpec, error) {
mainContainerIndex, mainContainer := k8s.GetContainerByName(template, d.Infra.MainContainerName)
if mainContainerIndex < 0 {
return template, fmt.Errorf("cannot find main container %q in spec", d.Infra.MainContainerName)
}
k8s.SetContainerImageTag(mainContainer, meta.Version)
k8s.ReplaceEnvVars(mainContainer, env_vars.GetStandard(meta))
template.Spec.Containers[mainContainerIndex] = *mainContainer
return template, nil
}
44 changes: 44 additions & 0 deletions aws/eks/iam_user_auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package eks

import (
"context"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
nsaws "github.com/nullstone-io/deployment-sdk/aws"
"github.com/nullstone-io/deployment-sdk/k8s"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
"sigs.k8s.io/aws-iam-authenticator/pkg/token"
)

var _ k8s.AuthInfoer = IamUserAuth{}

type IamUserAuth struct {
nsaws.User
Region string
ClusterId string
}

func (i IamUserAuth) AuthInfo(ctx context.Context) (clientcmdapi.AuthInfo, error) {
gen, err := token.NewGenerator(true, false)
if err != nil {
return clientcmdapi.AuthInfo{}, err
}
ses, err := session.NewSession(&aws.Config{
Credentials: credentials.NewStaticCredentials(i.User.AccessKeyId, i.User.SecretAccessKey, ""),
Region: aws.String(i.Region),
})
opts := &token.GetTokenOptions{
Region: i.Region,
ClusterID: i.ClusterId,
Session: ses,
}
tok, err := gen.GetWithOptions(opts)
if err != nil {
return clientcmdapi.AuthInfo{}, err
}

return clientcmdapi.AuthInfo{
Token: tok.Token,
}, nil
}
27 changes: 27 additions & 0 deletions aws/eks/log_streamer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package eks

import (
"context"
"github.com/nullstone-io/deployment-sdk/app"
"github.com/nullstone-io/deployment-sdk/k8s"
"github.com/nullstone-io/deployment-sdk/logging"
"github.com/nullstone-io/deployment-sdk/outputs"
"k8s.io/client-go/rest"
)

func NewLogStreamer(ctx context.Context, osWriters logging.OsWriters, source outputs.RetrieverSource, appDetails app.Details) (app.LogStreamer, error) {
outs, err := outputs.Retrieve[Outputs](ctx, source, appDetails.Workspace, appDetails.WorkspaceConfig)
if err != nil {
return nil, err
}

return k8s.LogStreamer{
OsWriters: osWriters,
Details: appDetails,
AppNamespace: outs.ServiceNamespace,
AppName: outs.ServiceName,
NewConfigFn: func(ctx context.Context) (*rest.Config, error) {
return CreateKubeConfig(ctx, outs.Region, outs.ClusterNamespace, outs.Deployer)
},
}, nil
}
68 changes: 68 additions & 0 deletions aws/eks/outputs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package eks

import (
"encoding/base64"
"fmt"
"github.com/nullstone-io/deployment-sdk/aws"
"github.com/nullstone-io/deployment-sdk/docker"
"github.com/nullstone-io/deployment-sdk/k8s"
apimachineryschema "k8s.io/apimachinery/pkg/runtime/schema"
restclient "k8s.io/client-go/rest"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
)

type Outputs struct {
ServiceNamespace string `ns:"service_namespace"`
ServiceName string `ns:"service_name"`
ImageRepoUrl docker.ImageUrl `ns:"image_repo_url,optional"`
ImagePusher nsaws.User `ns:"image_pusher,optional"`
Deployer nsaws.User `ns:"deployer,optional"`
MainContainerName string `ns:"main_container_name,optional"`
JobDefinitionName string `ns:"job_definition_name,optional"`

Region string `ns:"region"`
ClusterNamespace ClusterNamespaceOutputs `ns:",connectionContract:cluster-namespace/aws/kubernetes:eks,optional"`
}

func (o Outputs) ClusterArn() string {
return o.ClusterNamespace.ClusterArn
}

type ClusterInfoer interface {
k8s.ClusterInfoer
GetClusterName() string
}

var _ ClusterInfoer = ClusterNamespaceOutputs{}

type ClusterNamespaceOutputs struct {
ClusterName string `ns:"cluster_name"`
ClusterArn string `ns:"cluster_arn"`
ClusterEndpoint string `ns:"cluster_endpoint"`
ClusterCACertificate string `ns:"cluster_ca_certificate"`
}

func (o ClusterNamespaceOutputs) GetClusterName() string {
return o.ClusterName
}

func (o ClusterNamespaceOutputs) ClusterInfo() (clientcmdapi.Cluster, error) {
return GetClusterInfo(o.ClusterEndpoint, o.ClusterCACertificate)
}

func GetClusterInfo(endpoint, caCertificate string) (clientcmdapi.Cluster, error) {
decodedCACert, err := base64.StdEncoding.DecodeString(caCertificate)
if err != nil {
return clientcmdapi.Cluster{}, fmt.Errorf("invalid cluster CA certificate: %w", err)
}

host, _, err := restclient.DefaultServerURL(endpoint, "", apimachineryschema.GroupVersion{Group: "", Version: "v1"}, true)
if err != nil {
return clientcmdapi.Cluster{}, fmt.Errorf("failed to parse GKE cluster host %q: %w", endpoint, err)
}

return clientcmdapi.Cluster{
Server: host.String(),
CertificateAuthorityData: decodedCACert,
}, nil
}
Loading