DEV Community

Cover image for React app deployment on Kubernetes with kubectl, kustomize and helm in a multi-environment setup
Adrian Matei
Adrian Matei

Posted on

React app deployment on Kubernetes with kubectl, kustomize and helm in a multi-environment setup

Most applications depend on external factors that have different values depending on the environment where they are
deployed. We mostly use for that environment variables. Guess what? Most
of React Apps also have this need. In this blog posts presents a clean(er) way to make a multi-stage deployment of a Create React App on a Kubernetes Cluster. You can use this approach for a seamless integration into your continuous deployment pipeline.

In the beginning I will you show how to set up the React App and then guide you through several deployment possibilities on Kubernetes. You will deploy with native kubectl commands, with helm, with kustomize and in the end use skaffold.

The example app displays the latest public bookmarks published on www.bookmarks.dev. Depending on the environment the app is built for, it will display the environment name in the navigation bar and the header's color is different.

The source code is available on Github

TLDR;

Create a config.js file where you inject the environment variables in the window object (e.g. window.REACT_APP_API_URL='https://www.bookmarks.dev/api/public/bookmarks'). Add this file to the public folder of your react application. Dockerize the react application and at Kubernetes deployment time overwrite the config.js file in the container - you can do that with Kubernetes configMaps via native kubectl commands, kustomize or helm.

  • TOC {:toc}

Prerequisites

To run this application on Kubernetes locally make sure you have Docker Desktop with Kubernetes enabled, this is what I used for testing, or minikube installed. You can also deploy it directly in the cloud if you have an account.

React App Setup

The react application presented in this tutorial is build with create-react-app.

The public folder

You need to add a config.js
in the public folder. This will not be processed by webpack. Instead it will be copied into the build folder untouched. To reference the file in the public folder, you need to use the special variable called PUBLIC_URL:

    <head>
       .....
       <title>React App</title>
       <script src="%PUBLIC_URL%/config.js"></script>
     </head>
Enter fullscreen mode Exit fullscreen mode

The content of the config.js file:

window.REACT_APP_API_URL='https://www.bookmarks.dev/api/public/bookmarks'
window.REACT_APP_ENVIRONMENT='LOCAL'
window.REACT_APP_NAVBAR_COLOR='LightBlue'
Enter fullscreen mode Exit fullscreen mode

Usually the API_URL will point to a different URL depending on the environment, but here it is the same overall.

This was you can set your environment variables on the window object. These are the properties mentioned above. Make sure they are unique, so a good practice is to add the REACT_APP_ prefix as suggested in Adding Custom Environment Variables.

WARNING: Do not store any secrets (such as private API keys) in your React app! Environment variables are embedded into the build, meaning anyone can view them by inspecting your app's files.

At this point you can run and build the app locally the way you know it:

npm install 
npm start
Enter fullscreen mode Exit fullscreen mode

I recommend using nvm to run NodeJS locally

and then access it at http://localhost:3000

Why not use the process.env approach presented in Adding Custom Environment Variables

The runtime of static web-apps is the browser, where you don't have access process.env, so the values that are dependent on the environment have to be set prior to that, namely at build time.
If you do the deployment from your local machine, you can easily control the environment-variables - build the app for the environment you need and then deploy it. Tools like kustomize and skaffold, makes this feel like a breeze in the Kubernetes world as you'll find out later in the article.

But if you follow a continuous deployment approach, you'd usually have several steps, which form a so called pipeline:

  1. commit your code to a repository, hosted somewhere like GitHub
  2. your build system gets notified
  3. build system compiles the code and runs unit tests
  4. create image and push it to a registry, such as Docker Hub.
  5. from there you can deploy the image

The idea is to repeat as little steps as possible for the different environments. With the approach presented in this blog post, it will only be step number five (deployment), where we have environment specific configurations.

Containerize the application

First things first, let's build a docker container to use for the deployment on Kubernetes. Containerizing the application requires a base image to create an instance of the container.

Create the Dockerfile

The Dockerfile in the project root directory
contains the steps needed to build the Docker image:

# build environment
FROM node:12.9.0-alpine as build
WORKDIR /app

ENV PATH /app/node_modules/.bin:$PATH
COPY package.json /app/package.json
RUN npm install --silent
RUN npm config set unsafe-perm true #https://stackoverflow.com/questions/52196518/could-not-get-uid-gid-when-building-node-docker
RUN npm install react-scripts@3.0.1 -g --silent
COPY . /app
RUN npm run build

# production environment
FROM nginx:1.17.3-alpine
COPY --from=build /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Enter fullscreen mode Exit fullscreen mode

It uses a multi-stage build to build the docker image. In the first step you build the React APP on a node alpine image and in the second stepyou deploy it to an nginx-alpine image.

Build the docker image

To build the docker image run the following command in the project's root directory:

docker build --tag multi-stage-react-app-example:latest .
Enter fullscreen mode Exit fullscreen mode

At this point you can run the application in docker by issuing the following command:

docker run -p 3001:80 multi-stage-react-app-example:latest
Enter fullscreen mode Exit fullscreen mode

We forward nginx port 80 to 3001. Now you can access the application at http://localhost:3001

Note that the environment is LOCAL, as it uses the "original" config.js file

Push to docker repository

You can also push the image to a docker repository. Here is an example pushing it to the codepediaorg organisation on dockerhub:

docker tag multi-stage-react-app-example codepediaorg/multi-stage-react-app-example:latest
docker push codepediaorg/multi-stage-react-app-example:latest
Enter fullscreen mode Exit fullscreen mode

Deployment to Kubernetes

You can now take a docker container based on the image you've created and deploy it to kubernetes.

For that, all you need to do is create a Kubernetes service and deployment:

apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/component: service
  name: multi-stage-react-app-example
spec:
  ports:
    - name: http
      port: 80
      protocol: TCP
      targetPort: 80
  selector:
    app: multi-stage-react-app-example
  type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/component: service
  name: multi-stage-react-app-example
spec:
  replicas: 1
  selector:
    matchLabels:
      app: multi-stage-react-app-example
  template:
    metadata:
      labels:
        app.kubernetes.io/component: service
        app: multi-stage-react-app-example
    spec:
      containers:
        - name: multi-stage-react-app-example
          image: multi-stage-react-app-example:latest
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 80
Enter fullscreen mode Exit fullscreen mode

Kubernetes context and namespace

Before you run any kubectl apply command, it is important to know what context
and namespace you are applying your command against.

The easiest way to verify this, is to install kubectx and then issue kubectx to get
the current context and kubens for the current namespac. The default namespace is usually called default. In this blog post we operate on the local docker-desktop context and the default namespace.

Now that you know where your kubernetes objects will be applied to, you can add them to a file, like
deploy-to-kubernetes.yaml and apply the following the command:

kubectl apply -f deploy-to-kubernetes.yaml
Enter fullscreen mode Exit fullscreen mode

This will create the multi-stage-react-app-example service of type NodePort.
You can verify its presence by listing all services

kubeclt get svc
Enter fullscreen mode Exit fullscreen mode

or grep it with kubectl get svc | grep multi-stage-react-app-example

Port forward

To access the application inside the Kubernetes cluster you can use port-forwarding. The command to forward the service created before is

kubectl port-forward svc/multi-stage-react-app-example 3001:80
Enter fullscreen mode Exit fullscreen mode

Note svc before the service name

This commands forwards the local port 3001 to the container port 80 specified in the deployment file.

Now you can access the application inside the container at http://localhost:3001, which
uses the LOCAL environment.

You might want to hit Ctrl + Shift + R to force refresh the website in the browser (Chrome might have cached the old version)

Tear down created Kubernetes objects

To delete the service and deployment created, issue the following command

kubectl delete -f deploy-to-kubernetes.yaml
Enter fullscreen mode Exit fullscreen mode

Make the application deployment aware of the environment

Remember our purpose for continuous delivery pipeline: Make the application "aware" of the environment at deployment to cluster time.

Create a configMap

You start by creating a configMap.
We'll create one for the dev environment from the environment/dev.properties file:

kubectl create configmap multi-stage-react-app-example-config --from-file=config.js=environment/dev.properties
Enter fullscreen mode Exit fullscreen mode

This creates a configMap, which you can then reference by the config.js key and the content are the environment variables.

You can check this by issuing the following kubectl command:

kubectl get configmaps multi-stage-react-app-example-config -o yaml
Enter fullscreen mode Exit fullscreen mode

The result should look something like the following:

apiVersion: v1
data:
  config.js: |
    window.REACT_APP_API_URL='https://www.bookmarks.dev/api/public/bookmarks'
    window.REACT_APP_ENVIRONMENT='DEV'
    window.REACT_APP_NAVBAR_COLOR='LightGreen'
kind: ConfigMap
metadata:
  creationTimestamp: "2019-08-25T05:20:17Z"
  name: multi-stage-react-app-example-config
  namespace: default
  resourceVersion: "13382"
  selfLink: /api/v1/namespaces/default/configmaps/multi-stage-react-app-example-config
  uid: 06664d35-c6f8-11e9-8287-025000000001Å
Enter fullscreen mode Exit fullscreen mode

Mount the configMap in the container

The trick is now to mount the configMap into the container via a volume and overwrite the config.js file with the
values from the configMap
. Move now the configuration of the service and deployment resources in separate files in the kubernetes folder.
The deployment file:

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/component: service
  name: multi-stage-react-app-example
spec:
  replicas: 1
  selector:
    matchLabels:
      app: multi-stage-react-app-example
  template:
    metadata:
      labels:
        app.kubernetes.io/component: service
        app: multi-stage-react-app-example
    spec:
      containers:
        - name: multi-stage-react-app-example
          image: multi-stage-react-app-example:latest
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 80
          volumeMounts:
            - name:  multi-stage-react-app-example-config-volume
              mountPath: /usr/share/nginx/html/config.js
              subPath: config.js
              readOnly: true
      volumes:
        - name: multi-stage-react-app-example-config-volume
          configMap:
            name: multi-stage-react-app-example-config
Enter fullscreen mode Exit fullscreen mode

In the volumes section of the specification, define a volume based on the configMap you've just created:

      volumes:
        - name: multi-stage-react-app-example-config-volume
          configMap:
            name: multi-stage-react-app-example-config
Enter fullscreen mode Exit fullscreen mode

and then mount it in the container in the folder from where nginx delivers its files:

spec:
  ...
  template:
  ...
    metadata:
      labels:
        app.kubernetes.io/component: service
        app: multi-stage-react-app-example
    spec:
      containers:
        ...
          volumeMounts:
            - name:  multi-stage-react-app-example-config-volume
              mountPath: /usr/share/nginx/html/config.js
              subPath: config.js
              readOnly: true
Enter fullscreen mode Exit fullscreen mode

Note: you need to use subpath to only overwrite the config.js file, otherwise the content of the folder is replaced with this file

Deploy on kubernetes "dev" cluster

We will use the same local cluster to test our dev deployment. You apply now kubectl on
all the files in the kubernetes directory:

kubectl apply -f kubernetes
Enter fullscreen mode Exit fullscreen mode

Verify that the _config.js file has been replaced by connecting to the pod:

#first export list the pod holding our application
export MY_POD=`kubectl get pods | grep multi-stage-react-app-example | cut -f1 -d ' '`

# connect to shell in alpine image
kubectl exec -it $MY_POD -- /bin/sh 

# display content of the config.js file
less /usr/share/nginx/html/config.js 
Enter fullscreen mode Exit fullscreen mode

It should contain the variables for the dev environment:

window.REACT_APP_API_URL='https://www.bookmarks.dev/api/public/bookmarks'
window.REACT_APP_ENVIRONMENT='DEV'
window.REACT_APP_NAVBAR_COLOR='LightGreen'
Enter fullscreen mode Exit fullscreen mode

But better see it in action by port forwarding the application. You know now how it goes:

kubectl port-forward svc/multi-stage-react-app-example 3001:80
Enter fullscreen mode Exit fullscreen mode

Navigate to http://localhost:3001 and now you should see the DEV environment on the navigation bar.

In a continuous delivery pipeline you could have two steps:

  1. create the configMap based on the dev.properties file
  2. deploy on the target cluster with kubectl specified above

Tear down

kubectl delete -f kubernetes
Enter fullscreen mode Exit fullscreen mode

You can take the same approach for other environments, like test or staging.

Deploy on Kubernetes with Kustomize

What if now when deployment into the prod cluster you want to have two pods, instead of one serving the web app. Of course
you could modify the deployment.yaml file, specify 2 replicas instead of 1 and deploy. But you can solve this in an elegant
matter by using Kustomize, which provides other advantages too.

Kustomize is a standalone tool to customize Kubernetes objects through
a kustomization file. Since 1.14, Kubectl also supports the management of Kubernetes objects using a kustomization file, so you don't necessarily need to extra install it.
For this tutorial I suggest you do, as you'll need it later with Skaffold - on MacOS brew install kustomize

With Kustomize you define base resources in the so called bases (cross cutting concerns available in environments) and in the overlays the properties that are specific for the different deployments.
Here we place kustomize related files in the kustomize folder - tree kustomize:

kustomize/
├── base
│   ├── deployment.yaml
│   ├── kustomization.yaml
│   └── service.yaml
└── overlays
    ├── dev
    │   ├── dev.properties
    │   └── kustomization.yaml
    ├── local
    │   ├── kustomization.yaml
    │   └── local.properties
    └── prod
        ├── deployment-prod.yaml
        ├── kustomization.yaml
        └── prod.properties
Enter fullscreen mode Exit fullscreen mode

In the base folder we define the service and deployment, because in this case they are overall the same (except the 2 replicas for prod, but we'll deal with that later).

Deploy to dev cluster with Kustomize

Let's say we want to deploy to our dev cluster with Kustomize. For that we will use the dev overlays.
In the dev kustomization file:

bases:
  - ../../base

configMapGenerator:
  - name: multi-stage-react-app-example-config
    files:
      - dev.properties
Enter fullscreen mode Exit fullscreen mode

we point to the bases defined before and use the dev.properties file to generate the configMap.

Before we apply the dev overlay to the cluster we can check what it generates by issuing the following command:

kubectl kustomize kustomize/overlays/dev
Enter fullscreen mode Exit fullscreen mode

Note that the generated configMap name has a suffix (something like - multi-stage-react-app-example-config-gdgg4f85bt), which
is appended by hashing the contents of the file. This ensures that a new ConfigMap is generated when the content is changed. In the deploymant.yaml file the configMap is still referenced by multi-stage-react-app-example-config, but in the generated Deployment object it has the generated name.

To apply the "dev kustomization" use the following command:

kubectl apply -k kustomize/overlays/dev # <kustomization directory>
Enter fullscreen mode Exit fullscreen mode

Now port forward (kubectl port-forward svc/multi-stage-react-app-example 3001:80) and go to http://localhost:3001

Update an environment variable value

If you for example would like to update the value of an environment variable say, window.REACT_APP_NAVBAR_COLOR='Blue' in the dev.properties file,
what you need to do is apply gain the dev overlay:

kubectl apply -k kustomize/overlays/dev

#result similar to the following
configmap/multi-stage-react-app-example-config-dg44f5bkhh created
service/multi-stage-react-app-example unchanged
deployment.apps/multi-stage-react-app-example configured
Enter fullscreen mode Exit fullscreen mode

Note the a new configMap is created and is applied with the deployment. Reload and now the navigation bar is blue.

Tear down

kubectl delete -k kustomize/overlays/dev
Enter fullscreen mode Exit fullscreen mode

Deploy to production with kustomize

As mentioned before, maybe for production you would like to have two replicas delivering the application to achieve high availability. For that you can create an prod overlay that derives from that common base, similar as the dev overlay.

It defines extra an deployment-prod.yaml file:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: multi-stage-react-app-example
spec:
  replicas: 2
Enter fullscreen mode Exit fullscreen mode

which is a partial Deployment resource and we reference in the prod kustomization.yaml file
under patchesStrategicMerge:

bases:
  - ../../base

patchesStrategicMerge:
  - deployment-prod.yaml

configMapGenerator:
  - name: multi-stage-react-app-example-config
    files:
      - config.js=prod.properties
Enter fullscreen mode Exit fullscreen mode

You can see it's being modified by running:

kubectl kustomize kustomize/overlays/prod
Enter fullscreen mode Exit fullscreen mode

and then apply it:

kubectl apply -k kustomize/overlays/prod
Enter fullscreen mode Exit fullscreen mode

If you run kubectl get pods you should now see two entries, something like:

NAME                                             READY   STATUS    RESTARTS   AGE
multi-stage-react-app-example-59c5486dc4-2mjvw   1/1     Running   0          112s
multi-stage-react-app-example-59c5486dc4-s88ms   1/1     Running   0          112s
Enter fullscreen mode Exit fullscreen mode

Now you can port forward and access the application the way you know it

Tear down
kubectl delete -k kustomize/overlays/prod
Enter fullscreen mode Exit fullscreen mode

Deploy on Kubernetes with Helm

What is Helm? According to the documentation:

Helm is a tool that streamlines installing and managing Kubernetes applications. Think of it like apt/yum/homebrew for Kubernetes.

Helm uses the so called Kubernetes charts. Charts are packages of pre-configured Kubernetes resources. If you want to learn
more about Helm read the docs, we won't go into much details here, only punctual where it is needed.

At the moment Helm has a client (helm) and a server (tiller). Tiller runs inside of your Kubernetes cluster, and manages releases (installations)
of your charts.

Helm installation

On MacOS you can install the client with homebrew:

brew install kubernetes-helm
Enter fullscreen mode Exit fullscreen mode

For other platforms see Installing the Helm Client.

To install Tiller on your local Kubernetes cluster for testing just call the following command:

helm init

#result should something similar to the following:
Creating /Users/ama/.helm 
Creating /Users/ama/.helm/repository 
Creating /Users/ama/.helm/repository/cache 
Creating /Users/ama/.helm/repository/local 
Creating /Users/ama/.helm/plugins 
Creating /Users/ama/.helm/starters 
Creating /Users/ama/.helm/cache/archive 
Creating /Users/ama/.helm/repository/repositories.yaml 
Adding stable repo with URL: https://kubernetes-charts.storage.googleapis.com 
Adding local repo with URL: http://127.0.0.1:8879/charts 
$HELM_HOME has been configured at /Users/ama/.helm.

Tiller (the Helm server-side component) has been installed into your Kubernetes Cluster.

Please note: by default, Tiller is deployed with an insecure 'allow unauthenticated users' policy.
To prevent this, run `helm init` with the --tiller-tls-verify flag.
For more information on securing your installation see: https://docs.helm.sh/using_helm/#securing-your-helm-installation

Enter fullscreen mode Exit fullscreen mode

To check the helm version you can run then the following command:

$ helm version
Client: &version.Version{SemVer:"v2.14.3", GitCommit:"0e7f3b6637f7af8fcfddb3d2941fcc7cbebb0085", GitTreeState:"clean"}
Server: &version.Version{SemVer:"v2.14.3", GitCommit:"0e7f3b6637f7af8fcfddb3d2941fcc7cbebb0085", GitTreeState:"clean"}
Enter fullscreen mode Exit fullscreen mode

Helm setup in project

For this project the helm configuration is present in the helm-chart.

This was initially created via the helm create helm-chart command and adjusted for this app's needs.

Templates

The most important piece of the puzzle is the templates/ directory. This where Helm finds the YAML definitions for your
Services, Deployments and other Kubernetes resources.
Let's take a look at the service definition:

apiVersion: v1
kind: Service
metadata:
  name: {{ include "helm-chart.fullname" . }}
  labels:
    app.kubernetes.io/name: {{ include "helm-chart.name" . }}
    helm.sh/chart: {{ include "helm-chart.chart" . }}
    app.kubernetes.io/instance: {{ .Release.Name }}
    app.kubernetes.io/managed-by: {{ .Release.Service }}
spec:
  type: {{ .Values.service.type }}
  ports:
    - port: {{ .Values.service.port }}
      targetPort: http
      protocol: TCP
      name: http
  selector:
    app.kubernetes.io/name: {{ include "helm-chart.name" . }}
    app.kubernetes.io/instance: {{ .Release.Name }}
Enter fullscreen mode Exit fullscreen mode

It looks similar to the one used when installing with Kubectl or Kustomize, only that the values are substituted by Helm at deployment with the ones from Helm-specific objects.

Values

Values provide a way to override template defaults with your own configuration. They are present in the template via the .Values object as seen above.

Values can be set during helm install and helm upgrade operations, either by passing them in directly, or by uploading a values.yaml file.

The configMap

This time we will create the configMap as a Kubernetes object:

apiVersion: v1
kind: ConfigMap
metadata:
  name: multi-stage-react-app-example-config
  annotations:
    # https://github.com/helm/helm/blob/master/docs/charts_hooks.md
    "helm.sh/hook-delete-policy": "before-hook-creation"
    "helm.sh/hook": pre-install, pre-upgrade
data:
  config.js: {{ toYaml .Values.configValues | indent 4 }}
Enter fullscreen mode Exit fullscreen mode

We use helm hooks to create the configMap before installing or upgrading a helm chart ("helm.sh/hook": pre-install, pre-upgrade)

The thing is that the resources that a hook creates are not tracked or managed as part of the release. Once Tiller verifies that the hook has reached its ready state, it will leave the hook resource alon - thus you cannot rely upon helm delete to remove the resource. One way to destroy the resource is to add the "helm.sh/hook": pre-install, pre-upgrade annotation to the hook template file.

Deploy to local cluster with helm

Before deploying with helm you might want to examine the chart for possible issues and do a helm lint:

helm lint helm-chart
Enter fullscreen mode Exit fullscreen mode

and execute a dry-run to see the generated resources from the chart

helm install -n local-release helm-chart/ --dry-run --debug
Enter fullscreen mode Exit fullscreen mode

The result should be something like the following:

# result
[debug] Created tunnel using local port: '64528'

[debug] SERVER: "127.0.0.1:64528"

[debug] Original chart version: ""
[debug] CHART PATH: /Users/ama/projects/multi-stage-react-app-example/helm-chart

NAME:   local-release
REVISION: 1
RELEASED: Fri Aug 30 06:30:55 2019
CHART: helm-chart-0.1.0
USER-SUPPLIED VALUES:
{}

COMPUTED VALUES:
affinity: {}
configValues: |
  window.REACT_APP_API_URL='https://www.bookmarks.dev/api/public/bookmarks'
  window.REACT_APP_ENVIRONMENT='LOCAL with helm'
  window.REACT_APP_NAVBAR_COLOR='LightBlue'
fullnameOverride: ""
image:
  imagePullSecrets: cfcr
  pullPolicy: IfNotPresent
  repository: multi-stage-react-app-example
  tag: latest
ingress:
  annotations: {}
  enabled: false
  hosts:
  - chart-example.local
  paths: []
  tls: []
nameOverride: ""
nodeSelector: {}
replicaCount: 1
resources: {}
service:
  port: 80
  type: NodePort
tolerations: []

HOOKS:
---
# local-release-helm-chart-test-connection
apiVersion: v1
kind: Pod
metadata:
  name: "local-release-helm-chart-test-connection"
  labels:
    app.kubernetes.io/name: helm-chart
    helm.sh/chart: helm-chart-0.1.0
    app.kubernetes.io/instance: local-release
    app.kubernetes.io/managed-by: Tiller
  annotations:
    "helm.sh/hook": test-success
spec:
  containers:
    - name: wget
      image: busybox
      command: ['wget']
      args:  ['local-release-helm-chart:80']
  restartPolicy: Never
---
# local-release-multi-stage-react-app-example-config
apiVersion: v1
kind: ConfigMap
metadata:
  name: local-release-multi-stage-react-app-example-config
  annotations:
    # https://github.com/helm/helm/blob/master/docs/charts_hooks.md
    "helm.sh/hook-delete-policy": "before-hook-creation"
    "helm.sh/hook": pre-install, pre-upgrade
data:
  config.js:     |
      window.REACT_APP_API_URL='https://www.bookmarks.dev/api/public/bookmarks'
      window.REACT_APP_ENVIRONMENT='LOCAL with helm'
      window.REACT_APP_NAVBAR_COLOR='LightBlue'
MANIFEST:

---
# Source: helm-chart/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: local-release-helm-chart
  labels:
    app.kubernetes.io/name: helm-chart
    helm.sh/chart: helm-chart-0.1.0
    app.kubernetes.io/instance: local-release
    app.kubernetes.io/managed-by: Tiller
spec:
  type: NodePort
  ports:
    - port: 80
      targetPort: http
      protocol: TCP
      name: http
  selector:
    app.kubernetes.io/name: helm-chart
    app.kubernetes.io/instance: local-release
---
# Source: helm-chart/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: local-release-helm-chart
  labels:
    app.kubernetes.io/name: helm-chart
    helm.sh/chart: helm-chart-0.1.0
    app.kubernetes.io/instance: local-release
    app.kubernetes.io/managed-by: Tiller
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: helm-chart
      app.kubernetes.io/instance: local-release
  template:
    metadata:
      labels:
        app.kubernetes.io/name: helm-chart
        app.kubernetes.io/instance: local-release
    spec:
      imagePullSecrets:
        - name: cfcr
      containers:
        - name: helm-chart
          image: "multi-stage-react-app-example:latest"
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 80
              protocol: TCP
          livenessProbe:
            httpGet:
              path: /
              port: http
          readinessProbe:
            httpGet:
              path: /
              port: http
          volumeMounts:
            - name:  multi-stage-react-app-example-config-volume
              mountPath: /usr/share/nginx/html/config.js
              subPath: config.js
              readOnly: true
          resources:
            {}

      volumes:
        - name: multi-stage-react-app-example-config-volume
          configMap:
            name: local-release-multi-stage-react-app-example-config

Enter fullscreen mode Exit fullscreen mode

Note the names generated for service and deployment local-release-helm-chart (generated from {{ include "helm-chart.fullname" . }}) and
local-release-multi-stage-react-app-example-config (generated from {{ .Release.Name }}-multi-stage-react-app-example-config)

Now run the installation without the --dry-run flag for the actual installation:

helm install -n local-release helm-chart/
Enter fullscreen mode Exit fullscreen mode

Verify that the helm release is present by listing the helm releases (helm ls):

helm ls
NAME            REVISION        UPDATED                         STATUS          CHART                   APP VERSION     NAMESPACE
local-release   1               Fri Aug 30 06:46:09 2019        DEPLOYED        helm-chart-0.1.0        1.0             default 
Enter fullscreen mode Exit fullscreen mode

Now port-forward the service (you know how the service it's called from the dry run above local-release-helm-chart)

kubectl port-forward svc/local-release-helm-chart 3001:80
Enter fullscreen mode Exit fullscreen mode

and access the app at http://localhost:3001 with environment set to "LOCAL with helm"

Tear down helm release

helm delete --purge local-release
Enter fullscreen mode Exit fullscreen mode

Deploy with "dev" values

Now think you'd want to deploy to "dev" cluster. For that you can configure the environment values in a config-dev.yaml file:

configValues: |
  window.REACT_APP_API_URL='https://www.bookmarks.dev/api/public/bookmarks'
  window.REACT_APP_ENVIRONMENT='DEV'
  window.REACT_APP_NAVBAR_COLOR='LightGreen'
Enter fullscreen mode Exit fullscreen mode

which will be used at deployment to override the configValues from the values.yaml file. Use
the upsert variation this time, meaning that if the release is not present it will be created:

helm upgrade dev-release ./helm-chart/ --install --force --values helm-chart/config-values/config-dev.yaml
Enter fullscreen mode Exit fullscreen mode

Now port forward kubectl port-forward svc/dev-release-helm-chart 3001:80 and access the app at http://localhost:3001 et
voila you've deployed the dev environment.

Tear down dev-release

helm delete --purge dev-release
Enter fullscreen mode Exit fullscreen mode

Skaffold

This post was originally posted at A cleaner multi-stage continuous deployment on Kubernetes of a Create React App with kustomize, helm and skaffold - head out there to see how Skaffold can improve your local development and deployment on Kubernetes.

I would really appreciate if you had a look at the original www.bookmarks.dev application and give it a try (you might just love it) and star the generated public bookmarks at https://github.com/CodepediaOrg/bookmarks.

Oldest comments (3)

Collapse
 
tariqulislam profile image
Tariqul Islam Ronnie

Good writing.

Collapse
 
fburner profile image
FBurner

Using window destroys the lose coupling a bit because on multiple frontend services you end up with poluting the window with the config of every app.

I would have found it better if there is another solution using the build process.

Collapse
 
afq_d profile image
ashfaq dawood

how do I mock these window defined keys in my unit tests?