From f35598732bd0cdef3ccb7d34c2564288aa9d2c4d Mon Sep 17 00:00:00 2001 From: Remy DeWolf Date: Thu, 14 Apr 2022 14:37:08 -0700 Subject: [PATCH 01/12] Bring Your Own Kubernetes: MVP --- config/registry/helm/index.md | 14 +++++++++++ config/registry/helm/index.yaml | 24 ++++++++++++++++++ opta/commands/apply.py | 3 +++ opta/commands/destroy.py | 2 ++ opta/core/helm_cloud_client.py | 43 +++++++++++++++++++++++++++++++++ opta/core/kubernetes.py | 6 +++++ opta/core/terraform.py | 15 ++++++++++++ opta/core/validator.py | 16 ++++++++++++ opta/layer.py | 25 +++++++++++++++---- opta/registry.py | 5 +++- 10 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 config/registry/helm/index.md create mode 100644 config/registry/helm/index.yaml create mode 100644 opta/core/helm_cloud_client.py diff --git a/config/registry/helm/index.md b/config/registry/helm/index.md new file mode 100644 index 000000000..07b71d0a7 --- /dev/null +++ b/config/registry/helm/index.md @@ -0,0 +1,14 @@ +--- +title: "helm" +linkTitle: "helm" +date: 2022-04-11 +# we don't need to expose this in the documentation as a cloud provider +# this is an implementation detail when using byok +draft: true +weight: 1 +description: Bring Your Own Kubernetes +--- + +# Helm provider: Bring Your Own Kubernetes + +Use Opta with an existing Kubernetes. diff --git a/config/registry/helm/index.yaml b/config/registry/helm/index.yaml new file mode 100644 index 000000000..3d67128eb --- /dev/null +++ b/config/registry/helm/index.yaml @@ -0,0 +1,24 @@ +required_providers: + helm: + source: "hashicorp/helm" + version: "2.4.1" +backend: + local: + # this is consistent with terraform generator + path: "./tfstate/{layer_name}.tfstate" +validator: + name: str() + org_name: regex('^[a-z0-9-]{,15}$', name="Valid identifier, regex='[a-z0-9-]{,15}'") + modules: list(module()) + input_variables: list(map(), required=False) +service_validator: + - name: str() + modules: list(module()) + input_variables: list(map(), required=False) +module_aliases: + k8s-service: local-k8s-service + +output_providers: + helm: + kubernetes: + config_path: "{kubeconfig}" diff --git a/opta/commands/apply.py b/opta/commands/apply.py index 9beb76334..e463c78de 100644 --- a/opta/commands/apply.py +++ b/opta/commands/apply.py @@ -20,6 +20,7 @@ from opta.core.cloud_client import CloudClient from opta.core.gcp import GCP from opta.core.generator import gen, gen_opta_resource_tags +from opta.core.helm_cloud_client import HelmCloudClient from opta.core.kubernetes import cluster_exist, tail_module_log, tail_namespace_events from opta.core.local import Local from opta.core.plan_displayer import PlanDisplayer @@ -180,6 +181,8 @@ def _apply( if local: # boolean passed via cli pass cloud_client = Local(layer) + elif layer.cloud == "helm": + cloud_client = HelmCloudClient(layer) else: raise Exception(f"Cannot handle upload config for cloud {layer.cloud}") diff --git a/opta/commands/destroy.py b/opta/commands/destroy.py index 0d8d0c3ba..32e7aa50c 100644 --- a/opta/commands/destroy.py +++ b/opta/commands/destroy.py @@ -170,6 +170,8 @@ def _fetch_children_layers(layer: "Layer") -> List[str]: opta_configs = _azure_get_configs(layer) elif layer.cloud == "local": opta_configs = _local_get_configs(layer) + elif layer.cloud == "helm": + return [] else: raise Exception(f"Not handling deletion for cloud {layer.cloud}") diff --git a/opta/core/helm_cloud_client.py b/opta/core/helm_cloud_client.py new file mode 100644 index 000000000..30da46d7e --- /dev/null +++ b/opta/core/helm_cloud_client.py @@ -0,0 +1,43 @@ +from typing import TYPE_CHECKING, Dict, Optional + +from opta.core.cloud_client import CloudClient +from opta.exceptions import LocalNotImplemented +from opta.nice_subprocess import nice_run + +if TYPE_CHECKING: + from opta.layer import Layer, StructuredConfig + + +class HelmCloudClient(CloudClient): + def __init__(self, layer: "Layer"): + print(layer.root().providers) + super().__init__(layer) + + def get_remote_config(self) -> Optional["StructuredConfig"]: + return None + + def upload_opta_config(self) -> None: + return None + + def delete_opta_config(self) -> None: + return None + + def delete_remote_state(self) -> None: + return None + + def get_terraform_lock_id(self) -> str: + return "" + + def get_all_remote_configs(self) -> Dict[str, Dict[str, "StructuredConfig"]]: + raise LocalNotImplemented( + "get_all_remote_configs: Feature Unsupported for the helm provider" + ) + + def set_kube_config(self) -> None: + # do nothing, the user brings their own + pass + + def cluster_exist(self) -> bool: + # "kubectl version" returns an error code if it can't connect to a cluster + nice_run(["kubectl", "version"], check=True) + return True diff --git a/opta/core/kubernetes.py b/opta/core/kubernetes.py index 77b5fd36d..de33d4804 100644 --- a/opta/core/kubernetes.py +++ b/opta/core/kubernetes.py @@ -630,3 +630,9 @@ def restart_deployments(namespace: str) -> None: deployments = list_deployment(namespace) for deploy in deployments: restart_deployment(namespace, deploy.metadata.name) + + +def check_kubeconfig() -> None: + """check_kubeconfig verifies if there is a kubeconfig file defined, raises an error if not found""" + if not exists(expanduser(KUBE_CONFIG_DEFAULT_LOCATION)): + raise UserErrors(f"Could not find file '{KUBE_CONFIG_DEFAULT_LOCATION}'") diff --git a/opta/core/terraform.py b/opta/core/terraform.py index cdfd33902..f2a19e3c0 100644 --- a/opta/core/terraform.py +++ b/opta/core/terraform.py @@ -195,6 +195,8 @@ def verify_storage(cls, layer: "Layer") -> bool: return cls._azure_verify_storage(layer) elif layer.cloud == "local": return cls._local_verify_storage(layer) + elif layer.cloud == "helm": + return True else: raise Exception(f"Can not verify state storage for cloud {layer.cloud}") @@ -389,6 +391,16 @@ def download_state(cls, layer: "Layer") -> bool: except Exception: UserErrors(f"Could copy local state file to {state_file}") + elif layer.cloud == "helm": + if "local" in providers["terraform"]["backend"]: + try: + tf_file = providers["terraform"]["backend"]["local"]["path"] + if os.path.exists(tf_file): + copyfile(tf_file, state_file) + else: + return False + except Exception: + UserErrors(f"Could copy terraform state file to {state_file}") else: raise UserErrors("Need to get state from S3 or GCS or Azure storage") @@ -830,6 +842,9 @@ def delete_state_storage(cls, layer: "Layer") -> None: cloud_client = Azure(layer) elif layer.cloud == "local": cloud_client = Local(layer) + elif layer.cloud == "helm": + # There is no opta managed storage to delete + return else: raise Exception( f"Can not handle opta config deletion for cloud {layer.cloud}" diff --git a/opta/core/validator.py b/opta/core/validator.py index c9ac020ed..91e2fad89 100644 --- a/opta/core/validator.py +++ b/opta/core/validator.py @@ -58,6 +58,10 @@ class LocalModule(Module): cloud = "local" +class HelmModule(Module): + cloud = "helm" + + class Opta(Validator): """Opta Yaml Validator""" @@ -126,6 +130,12 @@ class LocalOpta(Opta): service_schema_dicts = REGISTRY["local"]["service_validator"] +class HelmOpta(Opta): + extra_validators = [HelmModule] + environment_schema_dict = REGISTRY["helm"]["validator"] + service_schema_dicts = REGISTRY["helm"]["service_validator"] + + def _get_yamale_errors( data: Any, schema_path: str, extra_validators: Optional[List[Type[Validator]]] = None ) -> List[str]: @@ -157,6 +167,8 @@ def _get_yamale_errors( azure_validators[AureOpta.tag] = AureOpta local_validators = DefaultValidators.copy() local_validators[LocalOpta.tag] = LocalOpta +helm_validators = DefaultValidators.copy() +helm_validators[HelmOpta.tag] = HelmOpta with NamedTemporaryFile(mode="w") as f: yaml.dump(REGISTRY["validator"], f) @@ -176,6 +188,9 @@ def _get_yamale_errors( local_main_schema = yamale.make_schema( f.name, validators=local_validators, parser="ruamel" ) + helm_main_schema = yamale.make_schema( + f.name, validators=helm_validators, parser="ruamel" + ) def _print_errors(errors: List[str]) -> None: @@ -201,6 +216,7 @@ def validate_yaml( "google": gcp_main_schema, "azurerm": azure_main_schema, "local": local_main_schema, + "helm": helm_main_schema, } DEFAULT_SCHEMA = vanilla_main_schema data = yamale.make_data(config_file_path, parser="ruamel") diff --git a/opta/layer.py b/opta/layer.py index 30321cf36..f11f9a9e3 100644 --- a/opta/layer.py +++ b/opta/layer.py @@ -29,6 +29,8 @@ from opta.core.azure import Azure from opta.core.cloud_client import CloudClient from opta.core.gcp import GCP +from opta.core.helm_cloud_client import HelmCloudClient +from opta.core.kubernetes import KUBE_CONFIG_DEFAULT_LOCATION, check_kubeconfig from opta.core.local import Local from opta.core.validator import validate_yaml from opta.crash_reporter import CURRENT_CRASH_REPORTER @@ -136,8 +138,18 @@ def __init__( self.original_spec = original_spec self.parent = parent self.path = path - if parent is None and org_name is None: - raise UserErrors("Config must have org name or a parent who has an org name") + self.cloud: str + if parent is None: + if len(providers) == 0: + # no parent, no provider = we are in helm (byok) mode + check_kubeconfig() + self.cloud = "helm" + # read the provider from the registry instead - the opta file doesn't define any with byok + providers = REGISTRY[self.cloud]["output_providers"] + elif org_name is None: + raise UserErrors( + "Config must have org name or a parent who has an org name" + ) self.org_name = org_name if self.parent and self.org_name is None: self.org_name = self.parent.org_name @@ -145,7 +157,6 @@ def __init__( total_base_providers = deep_merge( self.providers, self.parent.providers if self.parent else {} ) - self.cloud: str if "google" in total_base_providers and "aws" in total_base_providers: raise UserErrors( "You can have AWS as the cloud provider, or google, but not both" @@ -158,7 +169,8 @@ def __init__( self.cloud = "azurerm" elif "local" in total_base_providers: self.cloud = "local" - else: + + if not hasattr(self, "cloud"): raise UserErrors( "No cloud provider (AWS, GCP, or Azure) found, \n" + " or did you miss providing the --local flag for local deployment?" @@ -195,6 +207,8 @@ def get_cloud_client(self) -> CloudClient: return Azure(self) elif self.cloud == "local": return Local(self) + elif self.cloud == "helm": + return HelmCloudClient(self) else: raise Exception( f"Unknown cloud {self.cloud}. Can not handle getting the cloud client" @@ -532,6 +546,7 @@ def metadata_hydration(self) -> Dict[Any, Any]: "layer_name": self.name, "state_storage": self.state_storage(), "env": self.get_env(), + "kubeconfig": KUBE_CONFIG_DEFAULT_LOCATION, **provider_hydration, } @@ -639,7 +654,7 @@ def gen_providers(self, module_idx: int, clean: bool = True) -> Dict[Any, Any]: providers = deep_merge(providers, self.parent.providers) for cloud, provider in providers.items(): provider = self.handle_special_providers(cloud, provider, clean) - ret["provider"][cloud] = provider + ret["provider"][cloud] = hydrate(provider, hydration) if cloud in REGISTRY: ret["terraform"] = hydrate( {x: REGISTRY[cloud][x] for x in ["required_providers", "backend"]}, diff --git a/opta/registry.py b/opta/registry.py index 330cc4aa5..8dbade71a 100644 --- a/opta/registry.py +++ b/opta/registry.py @@ -25,7 +25,7 @@ def make_registry_dict() -> Dict[Any, Any]: with open(os.path.join(registry_path, "index.md"), "r") as f: registry_dict["text"] = f.read() module_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "modules") - for cloud in ["aws", "azurerm", "google", "local"]: + for cloud in ["aws", "azurerm", "google", "local", "helm"]: cloud_path = os.path.join(registry_path, cloud) cloud_dict = yaml.load(open(os.path.join(cloud_path, "index.yaml"))) cloud_dict["modules"] = {} @@ -35,6 +35,9 @@ def make_registry_dict() -> Dict[Any, Any]: alt_cloudname = "azure" elif cloud == "google": alt_cloudname = "gcp" + elif cloud == "helm": + # use the local modules since byok is cloud agnostic, we don't want to create resources like IAM and such + alt_cloudname = "local" else: alt_cloudname = cloud cloud_dict["modules"] = {**_make_module_registry_dict(module_path, alt_cloudname)} From 0af3752225268c9064cb54a543e1055baa66845d Mon Sep 17 00:00:00 2001 From: Remy DeWolf Date: Thu, 14 Apr 2022 16:04:16 -0700 Subject: [PATCH 02/12] unit tests --- opta/core/kubernetes.py | 6 --- opta/layer.py | 3 +- .../sample_opta_files/byok_service.yaml | 10 ++++ tests/test_byok.py | 47 +++++++++++++++++++ 4 files changed, 58 insertions(+), 8 deletions(-) create mode 100644 tests/fixtures/sample_opta_files/byok_service.yaml create mode 100644 tests/test_byok.py diff --git a/opta/core/kubernetes.py b/opta/core/kubernetes.py index de33d4804..77b5fd36d 100644 --- a/opta/core/kubernetes.py +++ b/opta/core/kubernetes.py @@ -630,9 +630,3 @@ def restart_deployments(namespace: str) -> None: deployments = list_deployment(namespace) for deploy in deployments: restart_deployment(namespace, deploy.metadata.name) - - -def check_kubeconfig() -> None: - """check_kubeconfig verifies if there is a kubeconfig file defined, raises an error if not found""" - if not exists(expanduser(KUBE_CONFIG_DEFAULT_LOCATION)): - raise UserErrors(f"Could not find file '{KUBE_CONFIG_DEFAULT_LOCATION}'") diff --git a/opta/layer.py b/opta/layer.py index f11f9a9e3..5625036e8 100644 --- a/opta/layer.py +++ b/opta/layer.py @@ -30,7 +30,7 @@ from opta.core.cloud_client import CloudClient from opta.core.gcp import GCP from opta.core.helm_cloud_client import HelmCloudClient -from opta.core.kubernetes import KUBE_CONFIG_DEFAULT_LOCATION, check_kubeconfig +from opta.core.kubernetes import KUBE_CONFIG_DEFAULT_LOCATION from opta.core.local import Local from opta.core.validator import validate_yaml from opta.crash_reporter import CURRENT_CRASH_REPORTER @@ -142,7 +142,6 @@ def __init__( if parent is None: if len(providers) == 0: # no parent, no provider = we are in helm (byok) mode - check_kubeconfig() self.cloud = "helm" # read the provider from the registry instead - the opta file doesn't define any with byok providers = REGISTRY[self.cloud]["output_providers"] diff --git a/tests/fixtures/sample_opta_files/byok_service.yaml b/tests/fixtures/sample_opta_files/byok_service.yaml new file mode 100644 index 000000000..62a37f78c --- /dev/null +++ b/tests/fixtures/sample_opta_files/byok_service.yaml @@ -0,0 +1,10 @@ +name: hello +org_name: opta-tests +modules: + - type: k8s-service + name: hello + port: + http: 80 + image: ghcr.io/run-x/opta-examples/hello-app:main + healthcheck_path: "/" + public_uri: "/hello" diff --git a/tests/test_byok.py b/tests/test_byok.py new file mode 100644 index 000000000..304f30446 --- /dev/null +++ b/tests/test_byok.py @@ -0,0 +1,47 @@ +# type: ignore +import os + +from modules.local_k8s_service.local_k8s_service import LocalK8sServiceProcessor +from opta.layer import Layer + + +class TestByok: + def test_all_good(self): + layer = Layer.load_from_yaml( + os.path.join( + os.getcwd(), + "tests", + "fixtures", + "sample_opta_files", + "byok_service.yaml", + ), + None, + ) + idx = len(layer.modules) + app_module = layer.get_module("hello", idx) + LocalK8sServiceProcessor(app_module, layer).process(idx) + + assert app_module.data["type"] == "k8s-service" + assert app_module.data["name"] == "hello" + assert app_module.data["image"] == "ghcr.io/run-x/opta-examples/hello-app:main" + assert app_module.data["public_uri"] == ["all/hello"] + assert app_module.data["env_name"] == "hello" + assert app_module.data["module_name"] == "hello" + + # check that the helm provider is generated correctly + assert layer.cloud == "helm" + assert layer.providers == { + "helm": {"kubernetes": {"config_path": "{kubeconfig}"}} + } + os.environ["KUBECONFIG"] = "~/.kube/custom-config" + gen_provider = layer.gen_providers(0) + print(gen_provider) + assert gen_provider["provider"] == { + "helm": {"kubernetes": {"config_path": "~/.kube/config"}} + } + assert gen_provider["terraform"]["required_providers"] == { + "helm": {"source": "hashicorp/helm", "version": "2.4.1"} + } + assert gen_provider["terraform"]["backend"] == { + "local": {"path": "./tfstate/hello.tfstate"} + } From b963308cb7db51b39949882279ba2bc948e8f2fa Mon Sep 17 00:00:00 2001 From: Remy DeWolf Date: Thu, 14 Apr 2022 16:11:57 -0700 Subject: [PATCH 03/12] fix test --- tests/test_layer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_layer.py b/tests/test_layer.py index 907f5e8d5..34c523631 100644 --- a/tests/test_layer.py +++ b/tests/test_layer.py @@ -43,6 +43,7 @@ def test_hydration_aws(self, mocker: MockFixture): assert layer.metadata_hydration() == { "aws": SimpleNamespace(region="us-east-1", account_id="011111111111"), "env": "dummy-parent", + "kubeconfig": "~/.kube/config", "layer_name": "dummy-config-1", "parent": SimpleNamespace( kms_account_key_arn="${data.terraform_remote_state.parent.outputs.kms_account_key_arn}", @@ -83,6 +84,7 @@ def test_hydration_gcp(self, mocker: MockFixture): ) assert layer.metadata_hydration() == { "env": "gcp-dummy-parent", + "kubeconfig": "~/.kube/config", "google": SimpleNamespace(region="us-central1", project="jds-throwaway-1"), "layer_name": "gcp-dummy-config", "parent": SimpleNamespace( From fddc0411e2257bf21253dfdf2c48e43db18dec7b Mon Sep 17 00:00:00 2001 From: Remy DeWolf Date: Thu, 14 Apr 2022 16:20:11 -0700 Subject: [PATCH 04/12] remove print statements --- opta/core/helm_cloud_client.py | 1 - tests/test_byok.py | 1 - 2 files changed, 2 deletions(-) diff --git a/opta/core/helm_cloud_client.py b/opta/core/helm_cloud_client.py index 30da46d7e..c706234c3 100644 --- a/opta/core/helm_cloud_client.py +++ b/opta/core/helm_cloud_client.py @@ -10,7 +10,6 @@ class HelmCloudClient(CloudClient): def __init__(self, layer: "Layer"): - print(layer.root().providers) super().__init__(layer) def get_remote_config(self) -> Optional["StructuredConfig"]: diff --git a/tests/test_byok.py b/tests/test_byok.py index 304f30446..458ce9030 100644 --- a/tests/test_byok.py +++ b/tests/test_byok.py @@ -35,7 +35,6 @@ def test_all_good(self): } os.environ["KUBECONFIG"] = "~/.kube/custom-config" gen_provider = layer.gen_providers(0) - print(gen_provider) assert gen_provider["provider"] == { "helm": {"kubernetes": {"config_path": "~/.kube/config"}} } From 5b4b1df598005247a67f09a6fefd1aef92177548 Mon Sep 17 00:00:00 2001 From: Remy DeWolf Date: Fri, 15 Apr 2022 09:37:00 -0700 Subject: [PATCH 05/12] add documentation --- examples/README.md | 1 + examples/byo-eks/README.md | 111 ---------- examples/byok-eks/README.md | 199 ++++++++++++++++++ examples/byok-eks/img/byok.png | Bin 0 -> 71952 bytes .../terraform/aws-lb-iam-policy.json | 0 .../terraform/aws-load-balancer-controller.tf | 0 .../{byo-eks => byok-eks}/terraform/data.tf | 0 .../terraform/ingress-nginx.tf | 0 .../terraform/linkerd.tf | 0 .../terraform/outputs.tf | 0 .../terraform/providers.tf | 0 .../terraform/variables.tf | 0 12 files changed, 200 insertions(+), 111 deletions(-) delete mode 100644 examples/byo-eks/README.md create mode 100644 examples/byok-eks/README.md create mode 100644 examples/byok-eks/img/byok.png rename examples/{byo-eks => byok-eks}/terraform/aws-lb-iam-policy.json (100%) rename examples/{byo-eks => byok-eks}/terraform/aws-load-balancer-controller.tf (100%) rename examples/{byo-eks => byok-eks}/terraform/data.tf (100%) rename examples/{byo-eks => byok-eks}/terraform/ingress-nginx.tf (100%) rename examples/{byo-eks => byok-eks}/terraform/linkerd.tf (100%) rename examples/{byo-eks => byok-eks}/terraform/outputs.tf (100%) rename examples/{byo-eks => byok-eks}/terraform/providers.tf (100%) rename examples/{byo-eks => byok-eks}/terraform/variables.tf (100%) diff --git a/examples/README.md b/examples/README.md index f1383181c..19c949190 100644 --- a/examples/README.md +++ b/examples/README.md @@ -4,6 +4,7 @@ This directory holds a compiled list of advanced, standalone, usages of Opta, mo - [airflow](/examples/airflow): Deploy Apache Airflow on AWS - [aws-lambda](/examples/aws-lambda): Deploy Apache Airflow on AWS +- [byok-eks](/examples/byok-eks): Bring Your Own Kubernetes, use Opta with an existing EKS - [flyte](/examples/flyte): Deploy Flyte on AWS - [full-stack-example](/examples/full-stack-example): Deploy a a todo list (including frontend, api, database, monitoring) locally or on AWS - [ghost](/examples/ghost): Deploy Ghost app on AWS diff --git a/examples/byo-eks/README.md b/examples/byo-eks/README.md deleted file mode 100644 index fddfe2bd5..000000000 --- a/examples/byo-eks/README.md +++ /dev/null @@ -1,111 +0,0 @@ -# Bring Your Own Cluster - -This is an example of using [Opta](https://github.com/run-x/opta) with an existing EKS cluster. - -# What does this do? - -If you already have an EKS cluster, and would like to try out Opta, follow these instructions to configure your cluster to work with Opta. - -If you don't have an EKS cluster, Opta can also create it, check [Getting Started](https://docs.opta.dev/getting-started/) instead. - -# What is included? - -By running terraform on an existing EKS cluster, your cluster will be configured to have the target [Network Architecture](https://docs.opta.dev/features/networking/network_overview/). - -The following components will be installed: - -- [Ingress Nginx](https://github.com/kubernetes/ingress-nginx) to expose services to the public -- [Linkerd](https://linkerd.io/) as the service mesh. -- [AWS Load Balancer Controller](https://kubernetes-sigs.github.io/aws-load-balancer-controller/) to manage the ELB for the Kubernetes cluster. - -Here is the break down of the terraform files: - - . - └── terraform - ├──aws-lb-iam-policy.json # The IAM policy for the load balancer - └──aws-load-balancer-controller.tf # Create IAM role and install the AWS Load Balancer Controller - └──data.tf # Data fetched from providers - └──ingress-nginx.tf # Install the Nginx Ingres Controller - └──linkerd.tf # Install Linkerd - └──outputs.tf # Terraform outputs - └──providers.tf # Terraform providers - └──variables.tf # Terraform variables - -# Requirements - -To configure the cluster (this guide), you need to use an AWS user with permissions to create AWS policies and roles, and admin permission on the target EKS cluster. - -# Configure the cluster for Opta - -This step configures the networking stack (nginx/linkerd/load balancer) on an existing EKS cluster. - -- Init terraform -```shell -cd ./terraform -terraform init -``` - -- [Optional] Configure a Terraform backend. By default, Terraform stores the state as a local file on disk. If you want to use a different backend such as S3, add this file locally. -```terraform -# ./terraform/backend.tf -terraform { - backend "s3" { - bucket = "mybucket" - key = "path/to/my/key" - region = "us-east-1" - } -} -``` -Check this [page](https://www.terraform.io/language/settings/backends) for more information or other backends. - -- Run terraformm plan -``` -terraform plan -var kubeconfig=~/.kube/config -var cluster_name=my-cluster -var oidc_provider_url=https://oidc.eks.... -out=tf.plan - -Plan: XX to add, 0 to change, 0 to destroy. -``` - -For the target EKS cluster: -- For `cluster_name`, run `aws eks list-clusters` to see the availables clusters. -- For `oidc_provider_url`, see `OpenID Connect provider URL` in the EKS cluster page in the AWS console. For more information, check the [official documentation](https://docs.aws.amazon.com/eks/latest/userguide/enable-iam-roles-for-service-accounts.html) -- For `kubeconfig`, check the [official documentation](https://docs.aws.amazon.com/eks/latest/userguide/create-kubeconfig.html) if you don't have one yet. - - -At this time, nothing was changed yet, you can review what will be created by terraform. - -- Run terraformm apply -``` -terraform apply tf.plan - -Apply complete! Resources: XX added, 0 changed, 0 destroyed. - -Outputs: - -load_balancer_raw_dns = "xxx" - -``` - -Note the load balancer DNS, this is the public endpoint to access your Kubernetes cluster. - -# Additional cluster configuration - -These steps are not automated with the terraform step, but you can configure them using these guides. -- Configure DNS - - Follow this guide: [Routing traffic to an ELB load balancer](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-to-elb-load-balancer.html) - - Using the TODO.. -- Configure a public certificate: - - Follow this guide: [Requesting a public certificate](https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-request-public.html) - - Using the certificate ARN, run the terraform commands with `-var load_balancer_cert_arn=...` - -# Deploy Kubernetes services with Opta - -Coming soon. - -# Uninstallation - -- Run terraformm destroy to remove the configuration added for Opta - -``` -terraform destroy -var kubeconfig=~/.kube/config -var cluster_name=my-cluster -var oidc_provider_url=https://oidc.eks.... -``` - diff --git a/examples/byok-eks/README.md b/examples/byok-eks/README.md new file mode 100644 index 000000000..9bab46bb4 --- /dev/null +++ b/examples/byok-eks/README.md @@ -0,0 +1,199 @@ +# Bring Your Own Cluster + +This is an example of using [Opta](https://github.com/run-x/opta) with an existing EKS cluster. + +# What does this do? + +This example provides Terraform files to configure Linkerd and Ingress Nginx controller in your EKS cluster to have the target [Network Architecture](https://docs.opta.dev/features/networking/network_overview/). +Once EKS is configured, you will be able to use Opta to deploy your service to Kubernetes. +Opta will generate the Terraform files and Helm chart for you, you only need to maintain the Opta file. + +![bring your own kubernetes](./img/byok.png) + + +# When to use this instead of full Opta? + +- Use this guide if you already have an EKS cluster, and would like to use Opta to deploy your Kubernetes services. +- If you don't have an EKS cluser, Opta can create it, check [Getting Started](https://docs.opta.dev/getting-started/) instead. + + +# What is included? + +The following components will be installed: + +- [Ingress Nginx](https://github.com/kubernetes/ingress-nginx) to expose services to the public +- [Linkerd](https://linkerd.io/) as the service mesh. +- [AWS Load Balancer Controller](https://kubernetes-sigs.github.io/aws-load-balancer-controller/) to manage the ELB for the Kubernetes cluster. + +Here is the break down of the terraform files: + + . + └── terraform + ├──aws-lb-iam-policy.json # The IAM policy for the load balancer + └──aws-load-balancer-controller.tf # Create IAM role and install the AWS Load Balancer Controller + └──data.tf # Data fetched from providers + └──ingress-nginx.tf # Install the Nginx Ingres Controller + └──linkerd.tf # Install Linkerd + └──outputs.tf # Terraform outputs + └──providers.tf # Terraform providers + └──variables.tf # Terraform variables + +# Requirements + +To configure the cluster (this guide), you need to use an AWS user with permissions to create AWS policies and roles, and admin permission on the target EKS cluster. + +Additionally, Opta only supports Linkerd for the service mesh at this time. + +# Configure the cluster for Opta + +This step configures the networking stack (nginx/linkerd/load balancer) on an existing EKS cluster. + +- Init terraform +```shell +cd ./terraform +terraform init +``` + +- [Optional] Configure a Terraform backend. By default, Terraform stores the state as a local file on disk. If you want to use a different backend such as S3, add this file locally. +```terraform +# ./terraform/backend.tf +terraform { + backend "s3" { + bucket = "mybucket" + key = "path/to/my/key" + region = "us-east-1" + } +} +``` +Check this [page](https://www.terraform.io/language/settings/backends) for more information or other backends. + +- Run terraformm plan +``` +terraform plan -var kubeconfig=~/.kube/config -var cluster_name=my-cluster -var oidc_provider_url=https://oidc.eks.... -out=tf.plan + +Plan: XX to add, 0 to change, 0 to destroy. +``` + +For the target EKS cluster: +- For `cluster_name`, run `aws eks list-clusters` to see the availables clusters. +- For `oidc_provider_url`, see `OpenID Connect provider URL` in the EKS cluster page in the AWS console. For more information, check the [official documentation](https://docs.aws.amazon.com/eks/latest/userguide/enable-iam-roles-for-service-accounts.html) +- For `kubeconfig`, check the [official documentation](https://docs.aws.amazon.com/eks/latest/userguide/create-kubeconfig.html) if you don't have one yet. + + +At this time, nothing was changed yet, you can review what will be created by terraform. + +- Run terraformm apply +``` +terraform apply tf.plan + +Apply complete! Resources: XX added, 0 changed, 0 destroyed. + +Outputs: + +load_balancer_raw_dns = "xxx" + +``` + +Note the load balancer DNS, this is the public endpoint to access your Kubernetes cluster. + +# Additional cluster configuration + +These steps are not automated with the terraform step, but you can configure them using these guides. +- Configure DNS + - Follow this guide: [Routing traffic to an ELB load balancer](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-to-elb-load-balancer.html) +- Configure a public certificate: + - Follow this guide: [Requesting a public certificate](https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-request-public.html) + - Using the certificate ARN, run the terraform commands with `-var load_balancer_cert_arn=...` + +# Configure your service to build and push a docker image + +Depending on which CI system you would like to use, configure your service to build and push docker images at each commit. +Here are some pointers for some popular systems: [Github Actions](https://github.com/marketplace/actions/build-and-push-docker-images), [CircleCI](https://circleci.com/docs/2.0/ecs-ecr/), [Jenkins](https://www.jenkins.io/doc/book/pipeline/docker/), [GitLab](https://docs.gitlab.com/ee/ci/docker/using_docker_build.html) + +# Deploy a service to Kubernetes with Opta + +Instead of having to define a Helm chart folder for each service, you can define one opta file that will take care of generating the Helm chart and use Terraform to apply the changes. + +1. Create the opta file + ```yaml + # hello.yaml + name: hello + org_name: runx + input_variables: + - name: image + modules: + - type: k8s-service + name: hello + port: + http: 80 + image: "{vars.image}" + healthcheck_path: "/" + public_uri: "/hello" + + ``` + +2. Run opta to deploy your service to your Kubernetes cluster + ```shell + # when running in CI, the image would be the one that was just pushed + opta apply -c hello.yaml --var image=ghcr.io/run-x/hello-opta/hello-opta:main + + ╒══════════╤══════════════════════════╤══════════╤════════╤══════════╕ + │ module │ resource │ action │ risk │ reason │ + ╞══════════╪══════════════════════════╪══════════╪════════╪══════════╡ + │ hello │ helm_release.k8s-service │ create │ LOW │ creation │ + ╘══════════╧══════════════════════════╧══════════╧════════╧══════════╛ + + ... + + Apply complete! Resources: 1 added, 0 changed, 0 destroyed. + + Outputs: + + # the image that is deployed will be confirmed here + current_image = "ghcr.io/run-x/hello-opta/hello-opta:main" + + Opta updates complete! + ``` + This step will: + - Generate the helm chart for your service + - Use the terraform helm provider to release the service + - Generate the terraform state files in `tfstate` - please commit these files after each apply. + +3. You can test that your service is deployed by using `kubectl` or `curl` to the public endpoint. +```shell +kubectl -n hello get svc,pod +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +service/hello ClusterIP 172.20.160.83 80/TCP 6m42s + +NAME READY STATUS RESTARTS AGE +pod/hello-hello-k8s-service-6d55889d55-cvjn8 2/2 Running 0 6m42s + +# use "load_balancer_raw_dns" from the step "Configure the cluster for Opta" +curl -k https://$load_balancer_raw_dns/hello +Hello from Opta! +``` + +# Delete a service from Kubernetes + +Use the opta `destroy` command to destroy your service. +All the Kubernetes resources for this service will be deleted. + +```shell +opta destroy -c hello.yaml + +╒══════════╤══════════════════════════╤══════════╤════════╤══════════╕ +│ module │ resource │ action │ risk │ reason │ +╞══════════╪══════════════════════════╪══════════╪════════╪══════════╡ +│ hello │ helm_release.k8s-service │ delete │ HIGH │ N/A │ +╘══════════╧══════════════════════════╧══════════╧════════╧══════════╛ + +Apply complete! Resources: 0 added, 0 changed, 1 destroyed. + +``` +# Uninstall Opta configuration from EKS + +- Run terraformm destroy to remove the configuration added for Opta + +``` +terraform destroy -var kubeconfig=~/.kube/config -var cluster_name=my-cluster -var oidc_provider_url=https://oidc.eks.... +``` diff --git a/examples/byok-eks/img/byok.png b/examples/byok-eks/img/byok.png new file mode 100644 index 0000000000000000000000000000000000000000..429d81d2fb3c213178da76ad765e2328f573d60a GIT binary patch literal 71952 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 t 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�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 Date: Fri, 15 Apr 2022 09:37:32 -0700 Subject: [PATCH 06/12] use hello-opta image --- tests/fixtures/sample_opta_files/byok_service.yaml | 2 +- tests/test_byok.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/fixtures/sample_opta_files/byok_service.yaml b/tests/fixtures/sample_opta_files/byok_service.yaml index 62a37f78c..0ac03989c 100644 --- a/tests/fixtures/sample_opta_files/byok_service.yaml +++ b/tests/fixtures/sample_opta_files/byok_service.yaml @@ -5,6 +5,6 @@ modules: name: hello port: http: 80 - image: ghcr.io/run-x/opta-examples/hello-app:main + image: ghcr.io/run-x/hello-opta/hello-opta:main healthcheck_path: "/" public_uri: "/hello" diff --git a/tests/test_byok.py b/tests/test_byok.py index 458ce9030..aa1896fb6 100644 --- a/tests/test_byok.py +++ b/tests/test_byok.py @@ -23,7 +23,7 @@ def test_all_good(self): assert app_module.data["type"] == "k8s-service" assert app_module.data["name"] == "hello" - assert app_module.data["image"] == "ghcr.io/run-x/opta-examples/hello-app:main" + assert app_module.data["image"] == "ghcr.io/run-x/hello-opta/hello-opta:main" assert app_module.data["public_uri"] == ["all/hello"] assert app_module.data["env_name"] == "hello" assert app_module.data["module_name"] == "hello" From 42da1da8030eb6efc77fc867221b31c1f4d2acf0 Mon Sep 17 00:00:00 2001 From: Remy DeWolf Date: Fri, 15 Apr 2022 12:30:31 -0700 Subject: [PATCH 07/12] implement get_kube_context_name --- opta/core/helm.py | 1 + opta/core/helm_cloud_client.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/opta/core/helm.py b/opta/core/helm.py index a0ce35997..e8e412921 100644 --- a/opta/core/helm.py +++ b/opta/core/helm.py @@ -1,3 +1,4 @@ +from posixpath import expanduser from subprocess import CalledProcessError # nosec from typing import FrozenSet, List, Optional diff --git a/opta/core/helm_cloud_client.py b/opta/core/helm_cloud_client.py index c706234c3..547442159 100644 --- a/opta/core/helm_cloud_client.py +++ b/opta/core/helm_cloud_client.py @@ -40,3 +40,8 @@ def cluster_exist(self) -> bool: # "kubectl version" returns an error code if it can't connect to a cluster nice_run(["kubectl", "version"], check=True) return True + + def get_kube_context_name(self) -> str: + return nice_run( + ["kubectl", "config", "current-context"], check=True, capture_output=True + ).stdout.strip() From d92d4d1d4ef4f0ba4e6571f8b90473ef3ab58a17 Mon Sep 17 00:00:00 2001 From: Remy DeWolf Date: Fri, 15 Apr 2022 12:36:45 -0700 Subject: [PATCH 08/12] Revert "add extra logs about lint commands" This reverts commit d4a64272619d5d99dbba6b2ce0fc38dd799779ae. --- scripts/lint.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/scripts/lint.py b/scripts/lint.py index a3d6a059d..3b0c50a76 100755 --- a/scripts/lint.py +++ b/scripts/lint.py @@ -5,7 +5,7 @@ import os import subprocess import sys -from typing import Collection, List +from typing import Collection from opta.json_schema import check_schemas @@ -57,25 +57,18 @@ def py_check(files_changed: Collection[str], precommit: bool, apply: bool) -> in flake8 = "pipenv run flake8 --exclude examples" mypy = "pipenv run mypy --exclude examples" - linters = [ - f"{linter} {' '.join(files_changed)}" for linter in [isort, black, flake8, mypy] - ] + cmd = f"{isort} {' '.join(files_changed)}\ + && {black} {' '.join(files_changed)}\ + && {flake8} {' '.join(files_changed)}\ + && {mypy} {' '.join(files_changed)}" logging.info("Running JSON schema check...") check_schemas(write=precommit or apply) logging.info("Running py checks...") - return execute_commands(linters) - - -def execute_commands(commands: List[str]) -> int: - if len(commands) == 0: - return 0 - cmd = commands[0] logging.info(cmd) - result = os.system(cmd) - logging.info("Success" if result == 0 else f"Error when running: {cmd}") - return result if result > 0 else execute_commands(commands[1:]) + + return os.system(cmd) if __name__ == "__main__": From 6a6d63d131e31d58cbaf82d20994c053a05e8c5e Mon Sep 17 00:00:00 2001 From: Remy DeWolf Date: Fri, 15 Apr 2022 15:21:42 -0700 Subject: [PATCH 09/12] fix typo --- opta/core/terraform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opta/core/terraform.py b/opta/core/terraform.py index f2a19e3c0..9248d6713 100644 --- a/opta/core/terraform.py +++ b/opta/core/terraform.py @@ -400,7 +400,7 @@ def download_state(cls, layer: "Layer") -> bool: else: return False except Exception: - UserErrors(f"Could copy terraform state file to {state_file}") + UserErrors(f"Could not copy terraform state file to {state_file}") else: raise UserErrors("Need to get state from S3 or GCS or Azure storage") From bfa1445e9f0f1c9bff5987ef891aa1847d93e035 Mon Sep 17 00:00:00 2001 From: Remy DeWolf Date: Wed, 20 Apr 2022 16:11:16 -0700 Subject: [PATCH 10/12] fix import after rebase --- opta/layer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opta/layer.py b/opta/layer.py index 5625036e8..cf615e9de 100644 --- a/opta/layer.py +++ b/opta/layer.py @@ -21,6 +21,7 @@ from google.auth import default from google.auth.exceptions import DefaultCredentialsError from google.oauth2 import service_account +from kubernetes.config.kube_config import KUBE_CONFIG_DEFAULT_LOCATION from modules.base import ModuleProcessor from modules.runx.runx import RunxProcessor @@ -30,7 +31,6 @@ from opta.core.cloud_client import CloudClient from opta.core.gcp import GCP from opta.core.helm_cloud_client import HelmCloudClient -from opta.core.kubernetes import KUBE_CONFIG_DEFAULT_LOCATION from opta.core.local import Local from opta.core.validator import validate_yaml from opta.crash_reporter import CURRENT_CRASH_REPORTER From aceae53257ff32f7e321c1b2a114d909d8539966 Mon Sep 17 00:00:00 2001 From: Remy DeWolf Date: Wed, 20 Apr 2022 17:00:39 -0700 Subject: [PATCH 11/12] fix lint, restore merged changes to lint.py --- opta/core/helm.py | 1 - scripts/lint.py | 21 ++++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/opta/core/helm.py b/opta/core/helm.py index e8e412921..a0ce35997 100644 --- a/opta/core/helm.py +++ b/opta/core/helm.py @@ -1,4 +1,3 @@ -from posixpath import expanduser from subprocess import CalledProcessError # nosec from typing import FrozenSet, List, Optional diff --git a/scripts/lint.py b/scripts/lint.py index 3b0c50a76..a3d6a059d 100755 --- a/scripts/lint.py +++ b/scripts/lint.py @@ -5,7 +5,7 @@ import os import subprocess import sys -from typing import Collection +from typing import Collection, List from opta.json_schema import check_schemas @@ -57,18 +57,25 @@ def py_check(files_changed: Collection[str], precommit: bool, apply: bool) -> in flake8 = "pipenv run flake8 --exclude examples" mypy = "pipenv run mypy --exclude examples" - cmd = f"{isort} {' '.join(files_changed)}\ - && {black} {' '.join(files_changed)}\ - && {flake8} {' '.join(files_changed)}\ - && {mypy} {' '.join(files_changed)}" + linters = [ + f"{linter} {' '.join(files_changed)}" for linter in [isort, black, flake8, mypy] + ] logging.info("Running JSON schema check...") check_schemas(write=precommit or apply) logging.info("Running py checks...") - logging.info(cmd) + return execute_commands(linters) + - return os.system(cmd) +def execute_commands(commands: List[str]) -> int: + if len(commands) == 0: + return 0 + cmd = commands[0] + logging.info(cmd) + result = os.system(cmd) + logging.info("Success" if result == 0 else f"Error when running: {cmd}") + return result if result > 0 else execute_commands(commands[1:]) if __name__ == "__main__": From ce36bf4539eaf0e99bf8e237a35cacd95976d92c Mon Sep 17 00:00:00 2001 From: Remy DeWolf Date: Wed, 20 Apr 2022 17:18:20 -0700 Subject: [PATCH 12/12] add warning about local state --- opta/core/helm_cloud_client.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/opta/core/helm_cloud_client.py b/opta/core/helm_cloud_client.py index 547442159..e05ad02d1 100644 --- a/opta/core/helm_cloud_client.py +++ b/opta/core/helm_cloud_client.py @@ -1,8 +1,11 @@ +import os from typing import TYPE_CHECKING, Dict, Optional +from opta.constants import REGISTRY from opta.core.cloud_client import CloudClient from opta.exceptions import LocalNotImplemented from opta.nice_subprocess import nice_run +from opta.utils import logger if TYPE_CHECKING: from opta.layer import Layer, StructuredConfig @@ -16,6 +19,13 @@ def get_remote_config(self) -> Optional["StructuredConfig"]: return None def upload_opta_config(self) -> None: + if "local" in REGISTRY[self.layer.cloud]["backend"]: + providers = self.layer.gen_providers(0) + local_path = providers["terraform"]["backend"]["local"]["path"] + real_path = os.path.dirname(os.path.realpath(local_path)) + logger.warning( + f"The terraform state is stored locally, make sure to keep the files in {real_path}" + ) return None def delete_opta_config(self) -> None: