Skip to content
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

support IAP setup for bootstrap on GKE #744

Merged
merged 2 commits into from
May 5, 2018
Merged
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
72 changes: 51 additions & 21 deletions bootstrap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,47 +21,57 @@ and based on the results chooses good values for various Kubeflow parameters.

**Alpha stage(as of today) Requires** run ```make build``` to build docker image locally since there's no public release yet.

Interactive use
**Enter interactive-use container**:

```
TAG=latest
APP_DIR_HOST=<Directory on host machine for the ksonnet apps>
APP_FOLDER_DOCKER=<Folder name inside docker for the ksonnet apps>
APP_DIR_HOST=$HOME/kfBootstrap
GITHUB_TOKEN=<Get a [GitHub Token](https://github.com/kubeflow/kubeflow/blob/master/user_guide.md#403-api-rate-limit-exceeded-error) to avoid API Limits>

# Start container
# Need to map config files like kubeconfig and gcloud config into the container.
docker run -ti \
-e GITHUB_TOKEN=${GITHUB_TOKEN} \
-e GROUP_ID=`id -g ${GROUP}` \
-e USER_ID=`id -u ${USER}` \
-e USER=${USER} \
-v ${APP_DIR_HOST}:/home/${USER}/${APP_FOLDER_DOCKER} \
-v ${APP_DIR_HOST}:/home/${USER}/kfBootstrap \
-v ${HOME}/.kube:/home/${USER}/.kube \
-v ${HOME}/.config:/home/${USER}/.config gcr.io/kubeflow-images-public/bootstrapper:latest
```

# Inside docker, run
/opt/kubeflow/bootstrapper --app-dir=/home/${USER}/${APP_FOLDER_DOCKER}/<your_app_name> --namespace=<Your new namespace which hold bootstrap>

# (Optional) enable usage reporting
**Inside container, choose one way to generate kubeflow apps**:
1. On GKE, with Google Sign-in enabled for kubeflow (share access with your team easily):
[Finish Preliminaries](README.md#iap-preliminaries)
```/opt/kubeflow/bootstrapper --app-dir=/home/${USER}/kfBootstrap/<your_app_name> --namespace=<new_namespace_for_bootstrap> --email=<GCP_account> --project=<GCP_project_containing_GKE>```
2. On GKE, without Google Sign-in:
```/opt/kubeflow/bootstrapper --app-dir=/home/${USER}/kfBootstrap/<your_app_name> --namespace=<new_namespace_for_bootstrap> --email=<GCP_account>```
3. Outside GKE:
```/opt/kubeflow/bootstrapper --app-dir=/home/${USER}/kfBootstrap/<your_app_name> --namespace=<new_namespace_for_bootstrap>```

Now the ksonnet app for deploying Kubeflow will be available in `${APP_DIR_HOST}/<your_app_name>`
**(Optional) enable usage reporting**
```
ks param set kubeflow-core reportUsage true
ks param set kubeflow-core usageId $(uuidgen)

# To deploy it
cd /home/${USER}/${APP_FOLDER_DOCKER}/<your_app_name>
ks apply default
```

#### To connect to your [Jupyter Notebook](http://jupyter.org/index.html) locally:
On host machine:
**To deploy it**
```
PODNAME=`kubectl get pods --namespace=<Namespace for bootstrap> --selector="app=tf-hub" --output=template --template="{{with index .items 0}}{{.metadata.name}}{{end}}"`
kubectl port-forward --namespace=<Namespace for bootstrap> $PODNAME 8000:8000
# Inside container:
cd /home/${USER}/kfBootstrap/<your_app_name>
ks apply default
```
Then, open [http://127.0.0.1:8000](http://127.0.0.1:8000) in your browser.

* After the tool runs the ksonnet app for deploying Kubeflow will be available in `${HOST_DIR}/${APP_NAME}`
* The user's home directory is mapped into the container so that
config files like kubeconfig and gcloud config are accessible.
**To connect to your [Jupyter Notebook](http://jupyter.org/index.html)**:
1. If you choose ```On GKE, with Google Sign-in enabled``` in generate app step:
Open ```https://kubeflow.endpoints.<project>.cloud.goog/hub```
2. Otherwise, you need to map port:
```
# On your local machine:
PODNAME=`kubectl get pods --namespace=<Namespace for bootstrap> --selector="app=tf-hub" --output=template --template="{{with index .items 0}}{{.metadata.name}}{{end}}"`
kubectl port-forward --namespace=<Namespace for bootstrap> $PODNAME 8000:8000
```
Then, open [http://127.0.0.1:8000](http://127.0.0.1:8000) in your browser.

## Explanation
For Kubeflow we want a **low bar and a high ceiling**.
Expand Down Expand Up @@ -157,6 +167,26 @@ Potential solutions
- e.g. if a pod depends on a volume or ConfigMap that pod won't be scheduled
until the config map exists

## IAP Preliminaries:
1. [Create external static IP address named kubeflow](https://github.com/kubeflow/kubeflow/blob/master/docs/gke/iap.md#create-an-external-static-ip-address)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be created automatically by the bootstrapper?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deployment manager should create it. This is next step.

2. [Enable Google Cloud Endpoint](https://console.cloud.google.com/apis/library/endpoints.googleapis.com/?q=Cloud%20Endpoints)
3. [Create oauth client credentials](https://github.com/kubeflow/kubeflow/blob/master/docs/gke/iap.md#create-oauth-client-credentials)
4. Create a service account an IAM binding:
```
gcloud iam service-accounts create cloud-endpoints-controller \
--display-name cloud-endpoints-controller
export SA_EMAIL=$(gcloud iam service-accounts list \
--filter="displayName:cloud-endpoints-controller" \
--format='value(email)')
gcloud projects add-iam-policy-binding \
$PROJECT --role roles/servicemanagement.admin --member serviceAccount:$SA_EMAIL

gcloud iam service-accounts keys create cloudep-sa.json --iam-account $SA_EMAIL

kubectl create secret generic --namespace=${NAMESPACE} cloudep-sa --from-file=./cloudep-sa.json
```
5. [Give access to kubeflow](https://github.com/kubeflow/kubeflow/blob/master/docs/gke/iap.md#adding-users)

## References

[Declarative Application Management in K8s](https://goo.gl/T66ZcD)
6 changes: 6 additions & 0 deletions bootstrap/cmd/bootstrap/app/options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ type ServerOption struct {
AppDir string
KfVersion string
NameSpace string
Project string
Email string
IpName string
}

// NewServerOption creates a new CMServer with a default config.
Expand All @@ -42,4 +45,7 @@ func (s *ServerOption) AddFlags(fs *flag.FlagSet) {
fs.StringVar(&s.KfVersion, "kubeflow-version", "v0.1.0-rc.4", "The Kubeflow version to use.")
fs.StringVar(&s.NameSpace, "namespace", "kubeflow", "The namespace where all resources for kubeflow will be created")
fs.BoolVar(&s.Apply, "apply", true, "Whether or not to apply the configuraiton.")
fs.StringVar(&s.Project, "project", "", "The GCP project where kubeflow will be installed")
fs.StringVar(&s.Email, "email", "", "Your Email address for GCP account, if you are using GKE.")
fs.StringVar(&s.IpName, "ip-name", "kubeflow", "Name of the ip you reserved on GCP project")
}
108 changes: 75 additions & 33 deletions bootstrap/cmd/bootstrap/app/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import (
k8sVersion "k8s.io/apimachinery/pkg/version"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"os/exec"
"errors"
)

// RecommendedConfigPathEnvVar is a environment variable for path configuration
Expand Down Expand Up @@ -198,6 +198,24 @@ func setupNamespace(namespaces type_v1.NamespaceInterface, name_space string) er
return err
}

func createComponent(opt *options.ServerOption, kfApp *kApp.App, fs *afero.Fs, args []string) {
componentName := args[1]
componentPath := filepath.Join(opt.AppDir, "components", componentName+".jsonnet")

if exists, _ := afero.Exists(*fs, componentPath); !exists {
log.Infof("Creating Component: %v ...", componentName)
err := actions.RunPrototypeUse(map[string]interface{}{
actions.OptionApp: *kfApp,
actions.OptionArguments: args,
})
if err != nil {
log.Fatalf("There was a problem creating protoype package kubeflow-core; error %v", err)
}
} else {
log.Infof("Component %v already exists", componentName)
}
}

// Run the tool.
func Run(opt *options.ServerOption) error {
// Check if the -version flag was passed and, if so, print the version and exit.
Expand Down Expand Up @@ -231,16 +249,13 @@ func Run(opt *options.ServerOption) error {
_, err = kubeClient.RbacV1().ClusterRoleBindings().Get(roleBindingName, meta_v1.GetOptions{})
if err != nil {
log.Infof("GKE: create rolebinding kubeflow-admin for role permission")
user, err := exec.Command("gcloud", "config", "get-value", "account").Output()
if err != nil {
return err
if opt.Email == "" {
return errors.New("Please provide --email YOUR_GCP_ACCOUNT")
}
username := strings.Trim(string(user), "\t\n ")

_, err = kubeClient.RbacV1().ClusterRoleBindings().Create(
&rbac_v1.ClusterRoleBinding{
ObjectMeta: meta_v1.ObjectMeta{Name: roleBindingName},
Subjects: []rbac_v1.Subject{{Kind: "User", Name: username}},
Subjects: []rbac_v1.Subject{{Kind: "User", Name: opt.Email}},
RoleRef: rbac_v1.RoleRef{Kind: "ClusterRole", Name: "cluster-admin"},
},
)
Expand Down Expand Up @@ -363,25 +378,8 @@ func Run(opt *options.ServerOption) error {
}

// Create the Kubeflow component
prototypeName := "kubeflow-core"
componentName := "kubeflow-core"
componentPath := filepath.Join(opt.AppDir, "components", componentName+".jsonnet")

if exists, _ := afero.Exists(fs, componentPath); !exists {
err = actions.RunPrototypeUse(map[string]interface{}{
actions.OptionApp: kfApp,
actions.OptionArguments: []string{
prototypeName,
componentName,
},
})
} else {
log.Infof("Component %v already exists", componentName)
}

if err != nil {
log.Fatalf("There was a problem creating protoype package kubeflow-core; error %v", err)
}
kubeflowCoreName := "kubeflow-core"
createComponent(opt, &kfApp, &fs, []string{kubeflowCoreName, kubeflowCoreName})

pvcMount := ""
if hasDefault {
Expand All @@ -390,7 +388,7 @@ func Run(opt *options.ServerOption) error {

err = actions.RunParamSet(map[string]interface{}{
actions.OptionApp: kfApp,
actions.OptionName: componentName,
actions.OptionName: kubeflowCoreName,
actions.OptionPath: "jupyterNotebookPVCMount",
actions.OptionValue: pvcMount,
})
Expand All @@ -399,17 +397,61 @@ func Run(opt *options.ServerOption) error {
return err
}

if isGke(clusterVersion) && opt.Project != "" {
log.Infof("Prepare Https access ...")

if !isGke(clusterVersion) {
return errors.New("Currently https auto setup only available on GKE.")
}
endpointsArgs := []string{
"cloud-endpoints",
"cloud-endpoints",
"--namespace",
opt.NameSpace,
"--secretName",
"cloudep-sa",
}
createComponent(opt, &kfApp, &fs, endpointsArgs)

certManagerArgs := []string{
"cert-manager",
"cert-manager",
"--namespace",
opt.NameSpace,
"--acmeEmail",
opt.Email,
}
createComponent(opt, &kfApp, &fs, certManagerArgs)

FQDN := fmt.Sprintf("kubeflow.endpoints.%v.cloud.goog", opt.Project)
iapIngressArgs := []string{
"iap-ingress",
"iap-ingress",
"--namespace",
opt.NameSpace,
"--ipName",
opt.IpName,
"--hostname",
FQDN,
}

createComponent(opt, &kfApp, &fs, iapIngressArgs)

err = actions.RunParamSet(map[string]interface{}{
actions.OptionApp: kfApp,
actions.OptionName: kubeflowCoreName,
actions.OptionPath: "jupyterHubAuthenticator",
actions.OptionValue: "iap",
})
if err != nil {
return err
}
}

if err := os.Chdir(kfApp.Root()); err != nil {
return err
}
log.Infof("App root %v", kfApp.Root())
err = actions.RunShow(map[string]interface{}{
actions.OptionApp: kfApp,
actions.OptionFormat: "yaml",
actions.OptionComponentNames: []string{},
actions.OptionEnvName: envName,
})

fmt.Printf("Initialized app %v\n", opt.AppDir)
return err
Expand Down
6 changes: 1 addition & 5 deletions kubeflow/core/prototypes/cert-manager.jsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,10 @@
local k = import "k.libsonnet";
local certManager = import "kubeflow/core/cert-manager.libsonnet";

local acmeEmail = import "param://acmeEmail";
local acmeUrl = import "param://acmeUrl";
local name = import "param://name";

// updatedParams uses the environment namespace if
// the namespace parameter is not explicitly set
local updatedParams = params {
namespace: if params.namespace == "null" then env.namespace else params.namespace,
};

certManager.parts(updatedParams.namespace).certManagerParts(acmeEmail, acmeUrl)
certManager.parts(updatedParams.namespace).certManagerParts(params.acmeEmail, params.acmeUrl)
5 changes: 1 addition & 4 deletions kubeflow/core/prototypes/cloud-endpoints.jsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,10 @@
local k = import "k.libsonnet";
local cloudEndpoints = import "kubeflow/core/cloud-endpoints.libsonnet";

local secretKey = import "param://secretKey";
local secretName = import "param://secretName";

// updatedParams uses the environment namespace if
// the namespace parameter is not explicitly set
local updatedParams = params {
namespace: if params.namespace == "null" then env.namespace else params.namespace,
};

cloudEndpoints.parts(updatedParams.namespace).cloudEndpointsParts(secretName, secretKey)
cloudEndpoints.parts(updatedParams.namespace).cloudEndpointsParts(params.secretName, params.secretKey)
12 changes: 2 additions & 10 deletions kubeflow/core/prototypes/iap-ingress.jsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,7 @@ local updatedParams = params {
namespace: if params.namespace == "null" then env.namespace else params.namespace,
};

local name = import "param://name";
local namespace = updatedParams.namespace;
local secretName = import "param://secretName";
local ipName = import "param://ipName";
local hostname = import "param://hostname";
local issuer = import "param://issuer";
local envoyImage = import "param://envoyImage";
local disableJwtCheckingParam = import "param://disableJwtChecking";
local disableJwtChecking = util.toBool(disableJwtCheckingParam);
local oauthSecretName = import "param://oauthSecretName";
local disableJwtChecking = util.toBool(params.disableJwtChecking);

iap.parts(namespace).ingressParts(secretName, ipName, hostname, issuer, envoyImage, disableJwtChecking, oauthSecretName)
iap.parts(namespace).ingressParts(params.secretName, params.ipName, params.hostname, params.issuer, params.envoyImage, disableJwtChecking, params.oauthSecretName)