One place for hosting & domains

      February 2019

      How To Configure Multi-Factor Authentication on Ubuntu 18.04


      The author selected the Electronic Frontier Foundation to receive a donation as part of the Write for DOnations program.

      Introduction

      Two-factor authentication (2FA) is an authentication method that requires entering more than one piece of information to successfully log in to an account or device. In addition to entering a username and password combination, 2FA requires the user to enter an additional piece of information such as a one-time password (OTP), like a six-digit verification code.

      In general, 2FA requires the user to enter information of different types:

      • Something that the user knows, such as a password
      • Something that the user has, such as the verification code generated from an authenticator application

      2FA is a subset of multi-factor authentication (MFA), which, in addition to something that the user knows and something that they have, requires something that the user is. This is the case of biometrics, which use technologies such as fingerprint or voice recognition.

      2FA helps strengthen the authentication process to a certain service or device: even if the password were compromised, an attacker would also need to have access to the user’s device which holds the authenticator app used to generate the security codes. For this reason, many online services (including DigitalOcean) offer the possibility to enable 2FA for user accounts to increase account security when it comes to the authentication phase.

      In this guide, you will configure 2FA for a non-root sudoer user on an Ubuntu 18.04 installation with the Google PAM module. Since you’re configuring 2FA on the non-root user, you will still be able to access the machine from your root account in case of a lockout. The tutorial will be general enough to be applied both to server and to desktop installations, both local and remote.

      Prerequisites

      Before you begin this guide you’ll need the following:

      • One Ubuntu 18.04 server or desktop environment. If you are using an Ubuntu server, set it up by following the Ubuntu 18.04 initial server setup guide, including a non-root user with sudo privileges and a firewall.

      • An authenticator application installed on your mobile device, with which you can scan 2FA QR codes, such as Google Authenticator or Authy.

      Step 1 — Installing the Google PAM Module

      In order to configure 2FA on Ubuntu 18.04, you need to install Google’s PAM module for Linux. The Pluggable Authentication Module (PAM) is the authentication mechanism Linux uses. You will use Google’s PAM module to have your user authenticate over 2FA using Google-generated OTP codes.

      First, log in as the non-root user that you configured in the prerequisites:

      Update the Ubuntu repositories to download the latest version of the authenticator:

      Now that your repositories are up to date, install the latest version of the PAM module:

      • sudo apt-get install libpam-google-authenticator

      This is a very small package with no dependencies, so it will take a few seconds to install. In the next section, you will configure 2FA for the non-root user on the system.

      Step 2 — Configuring 2FA for a User

      Now that you've installed the PAM module, you will run it to generate a QR code for the logged in user. This will create the code, but the Ubuntu environment won't require 2FA until you've enabled it later in this tutorial.

      Run the google-authenticator command to start and configure the PAM module:

      The command will present a prompt that will ask you several configuration questions. The first question will ask if you want tokens to be time based. Time-based authentication tokens will expire after a set amount of time, which defaults to 30 seconds on most systems. Time-based tokens are more secure than tokens that are not time-based, and most 2FA implementations use them. You can choose either option here, but this tutorial will choose Yes to use time-based authentication tokens:

      Output

      Do you want authentication tokens to be time-based (y/n) y

      After answering y to this question, you will see several lines output to your console:

      • A QR code: This is the code you need to scan using your authenticator app. Once you have scanned it, it will immediately turn into a code-generating device that will create a new OTP every 30 seconds.
      • Your secret key: This is an alternative method to configure your authenticator app. If you are using an app that does not support QR scanning, you can enter the secret key to configure your authentication app.
      • Your verification code: This is the first six-digit verification code that this specific QR code generates.
      • Your emergency scratch codes: Also known as backup codes, these one-use tokens will allow you to pass 2FA authentication if you lose your authenticator device. Keep these codes in a safe place to avoid being locked out of the account.

      After you've configured your authenticator app and saved your backup codes in a safe place, the prompt will ask if you'd like to update the configuration file. If you choose n, you will need to run the configuration program again. You will enter y to save your changes and move forward:

      Output

      Do you want me to update your "~/.google_authenticator" file (y/n) y

      The next question will ask if you want to disallow authentication codes to be used more than once. By default, you can only use each code once, even if it remains valid for 30 seconds. This is the safest choice because it prevents replay attacks from an attacker who somehow managed to get a hold of your verification code after you have used it. For this reason, it's more secure to disallow codes to be used more than once. Answer y to disallow multiple uses of the same token:

      Output

      Do you want to disallow multiple uses of the same authentication token? This restricts you to one login about every 30s, but it increases your chances to notice or even prevent man-in-the-middle attacks (y/n) y

      The next question asks if you want authentication tokens to be accepted a short time before or after their normal validity time. Verification codes are very time-sensitive, which means that your tokens can be refused if your devices are not synchronized. This option allows you to work around this issue by extending the default validity time of verification codes so that, even if your devices were temporarily out of sync, your authentication codes would be accepted anyway. Making sure that the time is the same on all your devices is the best option, as choosing yes will decrease the security of your system. Answer n to this question to not allow a grace period:

      Output

      By default, tokens are good for 30 seconds and in order to compensate for possible time-skew between the client and the server, we allow an extra token before and after the current time. If you experience problems with poor time synchronization, you can increase the window from its default size of 1:30min to about 4min. Do you want to do so (y/n) n

      The last question asks if you want to enable rate limiting for log in attempts. This will not allow more than three failed log in attempts every 30 seconds, which is a good security strengthening technique. Enable it by answering y:

      Output

      If the computer that you are logging into isn't hardened against brute-force login attempts, you can enable rate-limiting for the authentication module. By default, this limits attackers to no more than 3 login attempts every 30s. Do you want to enable rate-limiting (y/n) y

      You have now configured and generated 2FA codes for the non-root user with the PAM module. Now that your codes are generated, you need to enable 2FA in your environment.

      Step 3 — Activating 2FA in Ubuntu

      The Google PAM module is now generating 2FA codes your user, but Ubuntu doesn't yet know that it needs to use the codes as part of the user's authentication process. In this step, you will update Ubuntu's configuration to require 2FA tokens in addition to the regular method of authentication.

      You have two different options at this point:

      • You can require 2FA every time a user logs in to the system and every time a user requests sudo privileges.
      • You can require 2FA only during log in, where subsequent sudo authentication attempts would only require the user password.

      The first option will be ideal for a shared environment, where you may want to protect any actions that require sudo permissions. The second approach is more practical for a local desktop environment, where you are the only user on the system.

      Note: If you are enabling 2FA on a remote machine that you access over SSH, like a DigitalOcean Droplet, you need to follow steps two and three from the How To Set Up Multi-Factor Authentication for SSH on Ubuntu 16.04 guide before proceeding with this tutorial. The remaining steps in this tutorial apply to all Ubuntu installations, but remote environments need additional updates to make the SSH service aware of 2FA.

      If you are not using SSH to access your Ubuntu installation, you can immediately proceed with the remaining steps in the tutorial.

      Requiring 2FA for Log In and sudo Requests

      To be prompted for 2FA during log in and subsequent privilege escalation requests, you need to edit the /etc/pam.d/common-auth file by adding a line to the end of the existing file.

      The common-auth file applies to all authentication mechanisms on the system, regardless of the desktop environment used. It also applies to authentication requests that happen after the user logs in to the system, such as during a sudo escalation request when installing a new package from the terminal.

      Open this file with the following command:

      • sudo nano /etc/pam.d/common-auth

      Add the highlighted line at the end of the file:

      /etc/pam.d/common-auth

      ...
      # and here are more per-package modules (the "Additional" block)
      session required    pam_unix.so
      session optional    pam_systemd.so
      # end of pam-auth-update config
      auth required pam_google_authenticator.so nullok
      

      This line tells the Ubuntu authentication system to require 2FA upon log in through the Google PAM module. The nullok option allows existing users to log into the system even if they haven't configured 2FA authentication for their account. In other words, users who have configured 2FA will be required to enter an authentication code at the next log in, while users who haven't run the google-authenticator command will be able to log in with only their username and password until they configure 2FA.

      Save and close the file after adding the line.

      Requiring 2FA for Log In Only

      If you want to only be prompted for 2FA when you first log in to the system on a desktop environment, you need to edit the configuration file for the desktop manager you are using. The name of the configuration file usually matches the name of the desktop environment. For example, the configuration file for gdm, the default Ubuntu desktop environment starting after Ubuntu 16.04, is /etc/pam.d/gdm.

      In the case of a headless server, such as a DigitalOcean Droplet, you will edit the /etc/pam.d/common-session file instead. Open the relevant file based on your environment:

      • sudo nano /etc/pam.d/common-session

      Add the highlighted line to the end of the file:

      /etc/pam.d/common-session

      #
      # /etc/pam.d/common-session - session-related modules common to all services
      #
      ...
      # # and here are more per-package modules (the "Additional" block)
      session required    pam_unix.so
      session optional    pam_systemd.so
      # end of pam-auth-update config
      auth required pam_google_authenticator.so nullok
      

      This will tell Ubuntu to require 2FA when a user connects to the system via the command line (either locally or remotely through SSH), but not during subsequent authentication attempts, such as sudo requests.

      You have now successfully configured Ubuntu to prompt you for 2FA either just during log in, or for every authenticated action performed on the system. You're now ready to test the configuration and make sure that you are prompted for 2FA when you log in to your Ubuntu installation.

      Step 4 — Testing 2FA

      In the previous step, you've configured 2FA to generate codes every 30 seconds. In this step, you will test 2FA by logging into your Ubuntu environment.

      First, log out and back in to your Ubuntu environment:

      ssh sammy@your_server_ip
      

      If you are using password-based authentication, you will be prompted for your user password:

      Output

      Password:

      Note: If you are testing this on a DigitalOcean Droplet or another remote server protected by certificate authentication, you won’t be prompted for a password, and your key will be passed and accepted automatically. You will therefore only be prompted for your verification code.

      Enter your password and you will be prompted for the 2FA verification code:

      Output

      Verification code:

      After entering your verification code, you will be logged in:

      Output

      sammy@your_server_ip: ~#

      If 2FA was only enabled for logins, you won’t be prompted for your 2FA codes again until the session expires or you log out manually.

      If you enabled 2FA through the common-auth file, you will be prompted for it on every login and request for sudo privileges:

      Output

      sammy@your_server_ip: ~# sudo -s
 sudo password for sammy:
 Verification code: 
root@your_server_ip:

      In this step you have confirmed that your 2FA configuration is working as expected. If you were not prompted for your verification codes in this phase, return to step three of the tutorial and confirm that you have edited the correct Ubuntu authentication file.

      Step 5 — Preventing a 2FA Lockout

      In the event of a lost or wiped phone, it is important to have proper backup methods in place to recover access to your 2FA-enabled account. When you configure 2FA for the first time, you have a few options to ensure that you can recover from a lock out:

      • Save a backup copy of your secret configuration codes in a safe place. You can do this manually, but some authentication apps like Authy provide backup code features.
      • Save your recovery codes in a safe place that can be accessed outside of your 2FA enabled environment.

      If for any reason you don't have access to your backup options, you can take additional steps to recover access to your 2FA enabled local environment or remote server.

      Step 6 — Recovering from a 2FA Lockout on a Local Environment (Optional)

      If you have physical access to the machine, you can boot into rescue mode to disable 2FA. Rescue mode is a target type (similar to a runlevel) in Linux that is used to preform administrative tasks. You will need to edit some settings in GRUB, which is the default boot loader in Ubuntu, to enter rescue mode.

      To access GRUB, you will first reboot your machine:

      When the GRUB menu appears, make sure the Ubuntu entry is highlighted. This is the default name on a 18.04 installation, but may be different if you manually changed it after installation.

      The default GRUB menu in Ubuntu 18.04

      Next, press the e key on your keyboard to edit the GRUB configuration before booting into your system.

      The GRUB configuration file in edit mode

      In the file that appears, scroll down until you see a line that starts with linux and ends with $vt_handoff. Go to the end of this line and append systemd.unit=rescue.target, making sure that you leave a space between $vt_handoff and systemd.unit=rescue.target. This will tell your Ubuntu machine to boot into rescue mode.

      Editing the GRUB Configuration File to Enable Maintenance Mode

      Once you have made the changes, save the file with the Ctrl+X keyboard combination. Your machine will reboot and you will find yourself at a command line. Press Enter to go into rescue mode.

      Command line maintenance mode prompt in Ubuntu 18.04

      Once in rescue mode, open the Google Authenticator configuration file. This will be located inside the locked-out user’s home directory:

      • nano /home/sammy/.google-authenticator

      The first line in this file is the user’s secret key, which is used to configure an authenticator app.

      You now have two choices:

      • You can copy the secret key and configure your authenticator app.
      • If you want to start from a clean slate, you can delete the ~/.google-authenticator file altogether to disable 2FA for this user. After logging in again as the non-root user, you can configure 2FA once again and get a brand new secret key.

      With either choice, you are able to recover from a 2FA lockout on a local environment by using the GRUB boot loader. In the next step, you will recover from a 2FA lockout on a remote environment.

      Step 7 — Recovering from a 2FA Lockout on a Remote Environment (Optional)

      If your non-root sudoer account is locked out on a remote machine, you can use the root user to temporarily disable 2FA or reconfigure 2FA.

      Start by logging in to your machine with the root user:

      Once logged in, open the Google Authenticator settings file that is located inside the locked-out user’s home directory:

      • sudo nano /home/sammy/.google_authenticator

      The first line in this file is the user’s secret key, which is what you need to configure an authenticator app.

      You now have two choices:

      • If you want to set up a new or wiped device, you can use the secret key to reconfigure your authenticator app.
      • If you want to start from a clean slate, you can delete the /home/sammy/.google_authenticator file altogether to disable 2FA for this user. After logging in as the non-root user, you can configure 2FA once again and get a brand new secret key.

      With either choice, you were able to recover from a 2FA lockout on a local environment by using the root user.

      Conclusion

      In this tutorial, you configured 2FA on an Ubuntu 18.04 machine. With 2FA configured on your environment, you have added an extra layer of protection to your account and you have made your system more secure. In addition to your traditional authentication method, you will also have to enter an additional verification code to log in. This makes it impossible for an attacker who managed to acquire your login credentials to be able to log in to your account without this additional verification code.



      Source link

      Webinar Series: Kubernetes Package Management with Helm and CI/CD with Jenkins X


      Webinar Series

      This article supplements a webinar series on doing CI/CD with Kubernetes. The series discusses how to take a cloud native approach to building, testing, and deploying applications, covering release management, cloud native tools, service meshes, and CI/CD tools that can be used with Kubernetes. It is designed to help developers and businesses that are interested in integrating CI/CD best practices with Kubernetes into their workflows.

      This tutorial includes the concepts and commands from the second session of the series, Kubernetes Package Management with Helm and CI/CD with Jenkins X.

      Warning: The procedures in this tutorial are meant for demonstration purposes only. As a result, they don’t follow the best practices and security measures necessary for a production-ready deployment.

      Introduction

      In order to reduce error and organize complexity when deploying an application, CI/CD systems must include robust tooling for package management/deployment and pipelines with automated testing. But in modern production environments, the increased complexity of cloud-based infrastructure can present problems for putting together a reliable CI/CD environment. Two Kubernetes-specific tools developed to solve this problem are the Helm package manager and the Jenkins X pipeline automation tool.

      Helm is a package manager specifically designed for Kubernetes, maintained by the Cloud Native Computing Foundation (CNCF) in collaboration with Microsoft, Google, Bitnami, and the Helm contributor community. At a high level, it accomplishes the same goals as Linux system package managers like APT or YUM: managing the installation of applications and dependencies behind the scenes and hiding the complexity from the user. But with Kubernetes, the need for this kind of management is even more pronounced: Installing applications requires the complex and tedious orchestration of YAML files, and upgrading or rolling back releases can be anywhere from difficult to impossible. In order to solve this problem, Helm runs on top of Kubernetes and packages applications into pre-configured resources called charts, which the user can manage with simple commands, making the process of sharing and managing applications more user-friendly.

      Jenkins X is a CI/CD tool used to automate production pipelines and environments for Kubernetes. Using Docker images, Helm charts, and the Jenkins pipeline engine, Jenkins X can automatically manage releases and versions and promote applications between environments on GitHub.

      In this second article of the CI/CD with Kubernetes series, you will preview these two tools by:

      • Managing, creating, and deploying Kubernetes packages with Helm.

      • Building a CI/CD pipeline with Jenkins X.

      Though a variety of Kubernetes platforms can use Helm and Jenkins X, in this tutorial you will run a simulated Kubernetes cluster, set up in your local environment. To do this, you will use Minikube, a program that allows you to try out Kubernetes tools on your own machine without having to set up a true Kubernetes cluster.

      By the end of this tutorial, you will have a basic understanding of how these Kubernetes-native tools can help you implement a CI/CD system for your cloud application.

      Prerequisites

      To follow this tutorial, you will need:

      • An Ubuntu 16.04 server with 16 GB of RAM or above. Since this tutorial is meant for demonstration purposes only, commands are run from the root account. Note that the unrestrained privileges of this account do not adhere to production-ready best practices and could affect your system. For this reason, it is suggested to follow these steps in a test environment such as a virtual machine or a DigitalOcean Droplet.

      • A GitHub account and GitHub API token. Be sure to record this API token so that you can enter it during the Jenkins X portion of this tutorial.

      • Familiarity with Kubernetes concepts. Please refer to the article An Introduction to Kubernetes for more details.

      Step 1 — Creating a Local Kubernetes Cluster with Minikube

      Before setting up Minikube, you will have to install its dependencies, including the Kubernetes command line tool kubectl, the bidirectional data transfer relay socat, and the container program Docker.

      First, make sure that your system’s package manager can access packages over HTTPS with apt-transport-https:

      • apt-get update
      • apt-get install apt-transport-https

      Next, in order to ensure the kubectl download is valid, add the GPG key for the official Google repository to your system:

      • curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -

      Once you have added the GPG key, create the file /etc/apt/sources.list.d/kubernetes.list by opening it in your text editor:

      • nano /etc/apt/sources.list.d/kubernetes.list

      Once this file is open, add the following line:

      /etc/apt/sources.list.d/kubernetes.list

      deb http://apt.kubernetes.io/ kubernetes-xenial main
      

      This will show your system the source for downloading kubectl. Once you have added the line, save and exit the file. With the nano text editor, you can do this by pressing CTRL+X, typing y, and pressing ENTER.

      Finally, update the source list for APT and install kubectl, socat, and docker.io:

      • apt-get update
      • apt-get install -y kubectl socat docker.io

      Note: For Minikube to simulate a Kubernetes cluster, you must download the docker.io package rather than the newer docker-ce release. For production-ready environments, docker-ce would be the more appropriate choice, since it is better maintained in the official Docker repository.

      Now that you have installed kubectl, you can proceed with installing Minikube. First, use curl to download the program’s binary:

      • curl -Lo minikube https://storage.googleapis.com/minikube/releases/v0.28.0/minikube-linux-amd64

      Next, change the access permissions of the file you just downloaded so that your system can execute it:

      Finally, copy the minikube file to the executable path at /usr/local/bin/ and remove the original file from your home directory:

      • cp minikube /usr/local/bin/
      • rm minikube

      With Minikube installed on your machine, you can now start the program. To create a Minikube Kubernetes cluster, use the following command:

      • minikube start --vm-driver none

      The flag --vm-driver none instructs Minikube to run Kubernetes on the local host using containers rather than a virtual machine. Running Minikube this way means that you do not need to download a VM driver, but also means that the Kubernetes API server will run insecurely as root.

      Warning: Because the API server with root privileges will have unlimited access to the local host, it is not recommended to run Minikube using the none driver on personal workstations.

      Now that you have started Minikube, check to make sure that your cluster is running with the following command:

      You will receive the following output, with your IP address in place of your_IP_address:

      minikube: Running
      cluster: Running
      kubectl: Correctly Configured: pointing to minikube-vm at your_IP_address
      

      Now that you have set up your simulated Kubernetes cluster using Minikube, you can gain experience with Kubernetes package management by installing and configuring the Helm package manager on top of your cluster.

      Step 2 — Setting Up the Helm Package Manager on your Cluster

      In order to coordinate the installation of applications on your Kubernetes cluster, you will now install the Helm package manager. Helm consists of a helm client that runs outside the cluster and a tiller server that manages application releases from within the cluster. You will have to install and configure both to successfully run Helm on your cluster.

      To install the Helm binaries, first use curl to download the following installation script from the official Helm GitHub repository into a new file named get_helm.sh:

      • curl https://raw.githubusercontent.com/kubernetes/helm/master/scripts/get > get_helm.sh

      Since this script requires root access, change the permission of get_helm.sh so that the owner of the file (in this case, root) can read, write, and execute it:

      Now, execute the script:

      When the script finishes, you will have helm installed to /usr/local/bin/helm and tiller installed to /usr/local/bin/tiller.

      Though tiller is now installed, it does not yet have the correct roles and permissions to access the necessary resources in your Kubernetes cluster. To assign these roles and permissions to tiller, you will have to create a service account named tiller. In Kubernetes, a service account represents an identity for processes that run in a pod. After a process is authenticated through a service account, it can then contact the API server and access cluster resources. If a pod is not assigned a specific service account, it gets the default service account. You will also have to create a Role-Based access control (RBAC) rule that authorizes the tiller service account.

      In Kubernetes RBAC API, a role contains rules that determine a set of permissions. A role can be defined with a scope of namespace or cluster, and can only grant access to resources within a single namespace. ClusterRole can create the same permissions on the level of a cluster, granting access to cluster-scoped resources like nodes and namespaced resources like pods. To assign the tiller service account the right role, create a YAML file called rbac_helm.yaml and open it in your text editor:

      Add the following lines to the file to configure the tiller service account:

      rbac_helm.yaml

      apiVersion: v1
      kind: ServiceAccount
      metadata:
        name: tiller
        namespace: kube-system
      ---
      apiVersion: rbac.authorization.k8s.io/v1beta1
      kind: ClusterRoleBinding
      metadata:
        name: tiller
      roleRef:
        apiGroup: rbac.authorization.k8s.io
        kind: ClusterRole
        name: cluster-admin
      subjects:
        - kind: ServiceAccount
          name: tiller
          namespace: kube-system
      
        - kind: User
          name: "admin"
          apiGroup: rbac.authorization.k8s.io
      
        - kind: User
          name: "kubelet"
          apiGroup: rbac.authorization.k8s.io
      
        - kind: Group
          name: system:serviceaccounts
          apiGroup: rbac.authorization.k8s.io
      

      In the preceding file, ServiceAccount allows the tiller processes to access the apiserver as an authenticated service account. ClusterRole grants certain permissions to a role, and ClusterRoleBinding assigns that role to a list of subjects, including the tiller service account, the admin and kubelet users, and the system:serviceaccounts group.

      Next, deploy the configuration in rbac_helm.yaml with the following command:

      • kubectl apply -f rbac_helm.yaml

      With the tiller configuration deployed, you can now initialize Helm with the --service-acount flag to use the service account you just set up:

      • helm init --service-account tiller

      You will receive the following output, representing a successful initialization:

      Output

      Creating /root/.helm Creating /root/.helm/repository Creating /root/.helm/repository/cache Creating /root/.helm/repository/local Creating /root/.helm/plugins Creating /root/.helm/starters Creating /root/.helm/cache/archive Creating /root/.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 /root/.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 Happy Helming!

      This creates a tiller pod in the kube-system namespace. It also creates the .helm default repository in your $HOME directory and configures the default Helm stable chart repository at https://kubernetes-charts.storage.googleapis.com and the local Helm repository at http://127.0.0.1:8879/charts.

      To make sure that the tiller pod is running in the kube-system namespace, enter the following command:

      • kubectl --namespace kube-system get pods

      In your list of pods, tiller-deploy will appear, as is shown in the following output:

      Output

      NAME READY STATUS RESTARTS AGE etcd-minikube 1/1 Running 0 2h kube-addon-manager-minikube 1/1 Running 0 2h kube-apiserver-minikube 1/1 Running 0 2h kube-controller-manager-minikube 1/1 Running 0 2h kube-dns-86f4d74b45-rjql8 3/3 Running 0 2h kube-proxy-dv268 1/1 Running 0 2h kube-scheduler-minikube 1/1 Running 0 2h kubernetes-dashboard-5498ccf677-wktkl 1/1 Running 0 2h storage-provisioner 1/1 Running 0 2h tiller-deploy-689d79895f-bggbk 1/1 Running 0 5m

      If the tiller pod's status is Running, it can now manage Kubernetes applications from inside your cluster on behalf of Helm.

      To make sure that the entire Helm application is working, search the Helm package repositiories for an application like MongoDB:

      In the output, you will see a list of possible applications that fit your search term:

      Output

      NAME CHART VERSION APP VERSION DESCRIPTION stable/mongodb 5.4.0 4.0.6 NoSQL document-oriented database that stores JSON-like do... stable/mongodb-replicaset 3.9.0 3.6 NoSQL document-oriented database that stores JSON-like do... stable/prometheus-mongodb-exporter 1.0.0 v0.6.1 A Prometheus exporter for MongoDB metrics stable/unifi 0.3.1 5.9.29 Ubiquiti Network's Unifi Controller

      Now that you have installed Helm on your Kubernetes cluster, you can learn more about the package manager by creating a sample Helm chart and deploying an application from it.

      Step 3 — Creating a Chart and Deploying an Application with Helm

      In the Helm package manager, individual packages are called charts. Within a chart, a set of files defines an application, which can vary in complexity from a pod to a structured, full-stack app. You can download charts from the Helm repositories, or you can use the helm create command to create your own.

      To test out the capabilities of Helm, create a new Helm chart named demo with the following command:

      In your home directory, you will find a new directory called demo, within which you can create and edit your own chart templates.

      Move into the demo directory and use ls to list its contents:

      You will find the following files and directories in demo:

      demo

      charts  Chart.yaml  templates  values.yaml
      

      Using your text editor, open up the Chart.yaml file:

      Inside, you will find the following contents:

      demo/Chart.yaml

      apiVersion: v1
      appVersion: "1.0"
      description: A Helm chart for Kubernetes
      name: demo
      version: 0.1.0
      

      In this Chart.yaml file, you will find fields like apiVersion, which must be always v1, a description that gives additional information about what demo is, the name of the chart, and the version number, which Helm uses as a release marker. When you are done examining the file, close out of your text editor.

      Next, open up the values.yaml file:

      In this file, you will find the following contents:

      demo/values.yaml

      # Default values for demo.
      # This is a YAML-formatted file.
      # Declare variables to be passed into your templates.
      
      replicaCount: 1
      
      image:
        repository: nginx
        tag: stable
        pullPolicy: IfNotPresent
      
      nameOverride: ""
      fullnameOverride: ""
      
      service:
        type: ClusterIP
        port: 80
      
      ingress:
        enabled: false
        annotations: {}
          # kubernetes.io/ingress.class: nginx
          # kubernetes.io/tls-acme: "true"
        paths: []
        hosts:
          - chart-example.local
        tls: []
        #  - secretName: chart-example-tls
        #    hosts:
        #      - chart-example.local
      
      resources: {}
        # We usually recommend not to specify default resources and to leave this as a conscious
        # choice for the user. This also increases chances charts run on environments with little
        # resources, such as Minikube. If you do want to specify resources, uncomment the following
        # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
        # limits:
        #  cpu: 100m
        #  memory: 128Mi
        # requests:
        #  cpu: 100m
        #  memory: 128Mi
      
      nodeSelector: {}
      
      tolerations: []
      
      affinity: {}
      

      By changing the contents of values.yaml, chart developers can supply default values for the application defined in the chart, controlling replica count, image base, ingress access, secret management, and more. Chart users can supply their own values for these parameters with a custom YAML file using helm install. When a user provides custom values, these values will override the values in the chart’s values.yaml file.

      Close out the values.yaml file and list the contents of the templates directory with the following command:

      Here you will find templates for various files that can control different aspects of your chart:

      templates

      deployment.yaml  _helpers.tpl  ingress.yaml  NOTES.txt  service.yaml  tests
      

      Now that you have explored the demo chart, you can experiment with Helm chart installation by installing demo. Return to your home directory with the following command:

      Install the demo Helm chart under the name web with helm install:

      • helm install --name web ./demo

      You will get the following output:

      Output

      NAME: web LAST DEPLOYED: Wed Feb 20 20:59:48 2019 NAMESPACE: default STATUS: DEPLOYED RESOURCES: ==> v1/Service NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE web-demo ClusterIP 10.100.76.231 <none> 80/TCP 0s ==> v1/Deployment NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE web-demo 1 0 0 0 0s ==> v1/Pod(related) NAME READY STATUS RESTARTS AGE web-demo-5758d98fdd-x4mjs 0/1 ContainerCreating 0 0s NOTES: 1. Get the application URL by running these commands: export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=demo,app.kubernetes.io/instance=web" -o jsonpath="{.items[0].metadata.name}") echo "Visit http://127.0.0.1:8080 to use your application" kubectl port-forward $POD_NAME 8080:80

      In this output, you will find the STATUS of your application, plus a list of relevant resources in your cluster.

      Next, list the deployments created by the demo Helm chart with the following command:

      This will yield output that will list your active deployments:

      Output

      NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE web-demo 1 1 1 1 4m

      Listing your pods with the command kubectl get pods would show the pods that are running your web application, which would look like the following:

      Output

      NAME READY STATUS RESTARTS AGE web-demo-5758d98fdd-nbkqd 1/1 Running 0 4m

      To demonstrate how changes in the Helm chart can release different versions of your application, open up demo/values.yaml in your text editor and change replicaCount: to 3 and image:tag: from stable to latest. In the following code block, you will find what the YAML file should look like after you have finished modifying it, with the changes highlighted:

      demo/values.yaml

      # Default values for demo.
      # This is a YAML-formatted file.
      # Declare variables to be passed into your templates.
      
      replicaCount: 3
      
      image:
        repository: nginx
        tag: latest
        pullPolicy: IfNotPresent
      
      nameOverride: ""
      fullnameOverride: ""
      
      service:
        type: ClusterIP
        port: 80
      . . .
      

      Save and exit the file.

      Before you deploy this new version of your web application, list your Helm releases as they are now with the following command:

      You will receive the following output, with the one deployment you created earlier:

      Output

      NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE web 1 Wed Feb 20 20:59:48 2019 DEPLOYED demo-0.1.0 1.0 default

      Notice that REVISION is listed as 1, indicating that this is the first revision of the web application.

      To deploy the web application with the latest changes made to demo/values.yaml, upgrade the application with the following command:

      Now, list the Helm releases again:

      You will receive the following output:

      Output

      NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE web 2 Wed Feb 20 21:18:12 2019 DEPLOYED demo-0.1.0 1.0 default

      Notice that REVISION has changed to 2, indicating that this is the second revision.

      To find the history of the Helm releases for web, use the following:

      This will show both of the revisions of the web application:

      Output

      REVISION        UPDATED                         STATUS          CHART           DESCRIPTION
      1               Wed Feb 20 20:59:48 2019        SUPERSEDED      demo-0.1.0      Install complete
      2               Wed Feb 20 21:18:12 2019        DEPLOYED        demo-0.1.0      Upgrade complete
      

      To roll back your application to revision 1, enter the following command:

      This will yield the following output:

      Output

      Rollback was a success! Happy Helming!

      Now, bring up the Helm release history:

      You will receive the following list:

      Output

      REVISION UPDATED STATUS CHART DESCRIPTION 1 Wed Feb 20 20:59:48 2019 SUPERSEDED demo-0.1.0 Install complete 2 Wed Feb 20 21:18:12 2019 SUPERSEDED demo-0.1.0 Upgrade complete 3 Wed Feb 20 21:28:48 2019 DEPLOYED demo-0.1.0 Rollback to 1

      By rolling back the web application, you have created a third revision that has the same settings as revision 1. Remember, you can always tell which revision is active by finding the DEPLOYED item under STATUS.

      To prepare for the next section, clean up your testing area by deleting your web release with the helm delete command:

      Examine the Helm release history again:

      You will receive the following output:

      Output

      REVISION UPDATED STATUS CHART DESCRIPTION 1 Wed Feb 20 20:59:48 2019 SUPERSEDED demo-0.1.0 Install complete 2 Wed Feb 20 21:18:12 2019 SUPERSEDED demo-0.1.0 Upgrade complete 3 Wed Feb 20 21:28:48 2019 DELETED demo-0.1.0 Deletion complete

      The STATUS for REVISION 3 has changed to DELETED, indicating that your deployed instance of web has been deleted. However, although this does delete the release, it does not delete it from store. In order to delete the release completely, run the helm delete command with the --purge flag.

      In this step, you have managed application releases on Kubernetes with the Helm. If you would like to study Helm further, check out our An Introduction to Helm, the Package Manager for Kubernetes tutorial, or review the official Helm documentation.

      Next, you will set up and test the pipeline automation tool Jenkins X by using the jx CLI to create a CI/CD-ready Kubernetes cluster.

      Step 4 — Setting Up the Jenkins X Environment

      With Jenkins X, you can create your Kubernetes cluster from the ground up with pipeline automation and CI/CD solutions built in. By installing the jx CLI tool, you will be able to efficiently manage application releases, Docker images, and Helm charts, in addition to automatically promoting your applications across environments in GitHub.

      Since you will be using jx to create your cluster, you must first delete the Minikube cluster that you already have. To do this, use the following command:

      This will delete the local simulated Kubernete cluster, but will not delete the default directories created when you first installed Minikube. To clean these off your machine, use the following commands:

      • rm -rf ~/.kube
      • rm -rf ~/.minikube
      • rm -rf /etc/kubernetes/*
      • rm -rf /var/lib/minikube/*

      Once you have completely cleared Minikube from your machine, you can move on to installing the Jenkins X binary.

      First, download the compressed jx file from the official Jenkins X GitHub repository with the curl command and uncompress it with the tar command:

      • curl -L https://github.com/jenkins-x/jx/releases/download/v1.3.781/jx-linux-amd64.tar.gz | tar xzv

      Next, move the downloaded jx file to the executable path at /usr/local/bin:

      Jenkins X comes with a Docker Registry that runs inside your Kubernetes cluster. Since this is an internal element, security measures such as self-signed certificates can cause trouble for the program. To fix this, set Docker to use insecure registries for the local IP range. To do this, create the file /etc/docker/daemon.json and open it in your text editor:

      • nano /etc/docker/daemon.json

      Add the following contents to the file:

      /etc/docker/daemon.json

      {
        "insecure-registries" : ["0.0.0.0/0"]
      }
      

      Save and exit the file. For these changes to take effect, restart the Docker service with the following command:

      To verify that you have configured Docker with insecure registries, use the following command:

      At the end of the output, you should see the following highlighted line:

      Output

      Containers: 0 Running: 0 Paused: 0 Stopped: 0 Images: 15 Server Version: 18.06.1-ce Storage Driver: overlay2 Backing Filesystem: extfs Supports d_type: true Native Overlay Diff: true . . . Registry: https://index.docker.io/v1/ Labels: Experimental: false Insecure Registries: 0.0.0.0/0 127.0.0.0/8 Live Restore Enabled: false

      Now that you have downloaded Jenkins X and configured the Docker registry, use the jx CLI tool to create a Minikube Kubernetes cluster with CI/CD capabilities:

      • jx create cluster minikube --cpu=5 --default-admin-password=admin --vm-driver=none --memory=13314

      Here you are creating a Kubernetes cluster using Minikube, with the flag --cpu=5 to set 5 CPUs and --memory=13314 to give your cluster 13314 MBs of memory. Since Jenkins X is a robust but large program, these specifications will ensure that Jenkins X works without problems in this demonstration. Also, you are using --default-admin-password=admin to set the Jenkins X password as admin and --vm-driver=none to set up the cluster locally, as you did in Step 1.

      As Jenkins X spins up your cluster, you will receive various prompts at different times throughout the process that set the parameters for your cluster and determine how it will communicate with GitHub to manage your production environments.

      First, you will receive the following prompt:

      Output

      ? disk-size (MB) 150GB

      Press ENTER to continue. Next, you will be prompted for the name you wish to use with git, the email address you wish to use with git, and your GitHub username. Enter each of these when prompted, then press ENTER.

      Next, Jenkins X will prompt you to enter your GitHub API token:

      Output

      To be able to create a repository on GitHub we need an API Token Please click this URL https://github.com/settings/tokens/new?scopes=repo,read:user,read:org,user:email,write:repo_hook,delete_repo Then COPY the token and enter in into the form below: ? API Token:

      Enter your token here, or create a new token with the appropriate permissions using the highlighted URL in the preceding code block.

      Next, Jenkins X will ask:

      Output

      ? Do you wish to use GitHub as the pipelines Git server: (Y/n) ? Do you wish to use your_GitHub_username as the pipelines Git user for GitHub server: (Y/n)

      Enter Y for both questions.

      After this, Jenkins X will prompt you to answer the following:

      Output

      ? Select Jenkins installation type: [Use arrows to move, type to filter] >Static Master Jenkins Serverless Jenkins ? Pick workload build pack: [Use arrows to move, type to filter] > Kubernetes Workloads: Automated CI+CD with GitOps Promotion Library Workloads: CI+Release but no CD

      For the prior, select Static Master Jenkins, and select Kubernetes Workloads: Automated CI+CD with GitOps Promotion for the latter. When prompted to select an organization for your environment repository, select your GitHub username.

      Finally, you will receive the following output, which verifies successful installation and provides your Jenkins X admin password.

      Output

      Creating GitHub webhook for your_GitHub_username/environment-horsehelix-production for url http://jenkins.jx.your_IP_address.nip.io/github-webhook/ Jenkins X installation completed successfully ******************************************************** NOTE: Your admin password is: admin ******************************************************** Your Kubernetes context is now set to the namespace: jx To switch back to your original namespace use: jx namespace default For help on switching contexts see: https://jenkins-x.io/developing/kube-context/ To import existing projects into Jenkins: jx import To create a new Spring Boot microservice: jx create spring -d web -d actuator To create a new microservice from a quickstart: jx create quickstart

      Next, use the jx get command to receive a list of URLs that show information about your application:

      This command will yield a list similar to the following:

      Name                      URL
      jenkins                   http://jenkins.jx.your_IP_address.nip.io
      jenkins-x-chartmuseum     http://chartmuseum.jx.your_IP_address.nip.io
      jenkins-x-docker-registry http://docker-registry.jx.your_IP_address.nip.io
      jenkins-x-monocular-api   http://monocular.jx.your_IP_address.nip.io
      jenkins-x-monocular-ui    http://monocular.jx.your_IP_address.nip.io
      nexus                     http://nexus.jx.your_IP_address.nip.io
      

      You can use the URLs to view Jenkins X data about your CI/CD environment via a UI by entering the address into your browser and entering your username and password. In this case, this will be "admin" for both.

      Next, in order to ensure that the service accounts in the namespaces jx, jx-staging, and jx-production have admin privileges, modify your RBAC policies with the following commands:

      • kubectl create clusterrolebinding jx-staging1 --clusterrole=cluster-admin --user=admin --user=expose --group=system:serviceaccounts --serviceaccount=jx-staging:expose --namespace=jx-staging
      • kubectl create clusterrolebinding jx-staging2 --clusterrole=cluster-admin --user=admin --user=expose --group=system:serviceaccounts --serviceaccount=jx-staging:default --namespace=jx-staging
      • kubectl create clusterrolebinding jx-production1 --clusterrole=cluster-admin --user=admin --user=expose --group=system:serviceaccounts --serviceaccount=jx-production:expose --namespace=jx-productions
      • kubectl create clusterrolebinding jx-production2 --clusterrole=cluster-admin --user=admin --user=expose --group=system:serviceaccounts --serviceaccount=jx-production:default --namespace=jx-productions
      • kubectl create clusterrolebinding jx-binding1 --clusterrole=cluster-admin --user=admin --user=expose --group=system:serviceaccounts --serviceaccount=jx:expose --namespace=jx
      • kubectl create clusterrolebinding jx-binding2 --clusterrole=cluster-admin --user=admin --user=expose --group=system:serviceaccounts --serviceaccount=jx:default --namespace=jx

      Now that you have created your local Kubernetes cluster with Jenkins X functionality built in, you can move on to creating an application on the platform to test its CI/CD capabilities and experience a Jenkins X pipeline.

      Step 5 — Creating a Test Application in Your Jenkins X Environment

      With your Jenkins X environment set up in your Kubernetes cluster, you now have CI/CD infrastructure in place that can help you automate a testing pipeline. In this step, you will try this out by setting up a test application in a working Jenkins X pipeline.

      For demonstration purposes, this tutorial will use a sample RSVP application created by the CloudYuga team. You can find this application, along with other webinar materials, at the DO-Community GitHub repository.

      First, clone the sample application from the repository with the following command:

      • git clone https://github.com/do-community/rsvpapp.git

      Once you've cloned the repository, move into the rsvpapp directory and remove the git files:

      To initialize a git repository and a Jenkins X project for a new application, you can use jx create to start from scratch or a template, or jx import to import an existing application from a local project or git repository. For this tutorial, import the sample RSVP application by running the following command from within the application's home directory:

      Jenkins X will prompt you for your GitHub username, whether you'd like to initialize git, a commit message, your organization, and the name you would like for your repository. Answer yes to initialize git, then provide the rest of the prompts with your individual GitHub information and preferences. As Jenkins X imports the application, it will create Helm charts and a Jenkinsfile in your application's home directory. You can modify these charts and the Jenkinsfile as per your requirements.

      Since the sample RSVP application runs on port 5000 of its container, modify your charts/rsvpapp/values.yaml file to match this. Open the charts/rsvpapp/values.yaml in your text editor:

      • nano charts/rsvpapp/values.yaml

      In this values.yaml file, set service:internalPort: to 5000. Once you have made this change, your file should look like the following:

      charts/rsvpapp/values.yaml

      # Default values for python.
      # This is a YAML-formatted file.
      # Declare variables to be passed into your templates.
      replicaCount: 1
      image:
        repository: draft
        tag: dev
        pullPolicy: IfNotPresent
      service:
        name: rsvpapp
        type: ClusterIP
        externalPort: 80
        internalPort: 5000
        annotations:
          fabric8.io/expose: "true"
          fabric8.io/ingress.annotations: "kubernetes.io/ingress.class: nginx"
      resources:
        limits:
          cpu: 100m
          memory: 128Mi
        requests:
          cpu: 100m
          memory: 128Mi
      ingress:
        enabled: false
      

      Save and exit your file.

      Next, change the charts/preview/requirements.yaml to fit with your application. requirements.yaml is a YAML file in which developers can declare chart dependencies, along with the location of the chart and the desired version. Since our sample application uses MongoDB for database purposes, you'll need to modify the charts/preview/requirements.yaml file to list MongoDB as a dependency. Open the file in your text editor with the following command:

      • nano charts/preview/requirements.yaml

      Edit the file by adding the mongodb-replicaset entry after the alias: cleanup entry, as is highlighted in the following code block:

      charts/preview/requirements.yaml

      # !! File must end with empty line !!
      dependencies:
      - alias: expose
        name: exposecontroller
        repository: http://chartmuseum.jenkins-x.io
        version: 2.3.92
      - alias: cleanup
        name: exposecontroller
        repository: http://chartmuseum.jenkins-x.io
        version: 2.3.92
      - name: mongodb-replicaset
        repository: https://kubernetes-charts.storage.googleapis.com/
        version: 3.5.5
      
        # !! "alias: preview" must be last entry in dependencies array !!
        # !! Place custom dependencies above !!
      - alias: preview
        name: rsvpapp
        repository: file://../rsvpapp
      

      Here you have specified the mongodb-replicaset chart as a dependency for the preview chart.

      Next, repeat this process for your rsvpapp chart. Create the charts/rsvpapp/requirements.yaml file and open it in your text editor:

      • nano charts/rsvpapp/requirements.yaml

      Once the file is open, add the following, making sure that there is a single line of empty space before and after the populated lines:

      charts/rsvpapp/requirements.yaml

      
      dependencies:
      - name: mongodb-replicaset
        repository: https://kubernetes-charts.storage.googleapis.com/
        version: 3.5.5
      
      

      Now you have specified the mongodb-replicaset chart as a dependency for your rsvpapp chart.

      Next, in order to connect the frontend of the sample RSVP application to the MongoDB backend, add a MONGODB_HOST environment variable to your deployment.yaml file in charts/rsvpapp/templates/. Open this file in your text editor:

      • nano charts/rsvpapp/templates/deployment.yaml

      Add the following highlighted lines to the file, in addition to one blank line at the top of the file and two blank lines at the bottom of the file. Note that these blank lines are required for the YAML file to work:

      charts/rsvpapp/templates/deployment.yaml

      
      apiVersion: extensions/v1beta1
      kind: Deployment
      metadata:
        name: {{ template "fullname" . }}
        labels:
          draft: {{ default "draft-app" .Values.draft }}
          chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}"
      spec:
        replicas: {{ .Values.replicaCount }}
        template:
          metadata:
            labels:
              draft: {{ default "draft-app" .Values.draft }}
              app: {{ template "fullname" . }}
      {{- if .Values.podAnnotations }}
            annotations:
      {{ toYaml .Values.podAnnotations | indent 8 }}
      {{- end }}
          spec:
            containers:
            - name: {{ .Chart.Name }}
              image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
              env:
              - name: MONGODB_HOST
                value: "mongodb://{{.Release.Name}}-mongodb-replicaset-0.{{.Release.Name}}-mongodb-replicaset,{{.Release.Name}}-mongodb-replicaset-1.{{.Release.Name}}-mongodb-replicaset,{{.Release.Name}}-mongodb-replicaset-2.{{.Release.Name}}-mongodb-replicaset:27017"
              imagePullPolicy: {{ .Values.image.pullPolicy }}
              ports:
              - containerPort: {{ .Values.service.internalPort }}
              resources:
      {{ toYaml .Values.resources | indent 12 }}
      
      
      

      With these changes, Helm will be able to deploy your application with MongoDB as its database.

      Next, examine the Jenkinsfile generated by Jenkins X by opening the file from your application's home directory:

      This Jenkinsfile defines the pipeline that is triggered every time you commit a version of your application to your GitHub repository. If you wanted to automate your code testing so that the tests are triggered every time the pipeline is triggered, you would add the test to this document.

      To demonstrate this, add a customized test case by replacing sh "python -m unittest" under stage('CI Build and push snapshot') and stage('Build Release') in the Jenkinsfile with the following highlighted lines:

      /rsvpapp/Jenkinsfile

      . . .
        stages {
          stage('CI Build and push snapshot') {
            when {
              branch 'PR-*'
            }
            environment {
              PREVIEW_VERSION = "0.0.0-SNAPSHOT-$BRANCH_NAME-$BUILD_NUMBER"
              PREVIEW_NAMESPACE = "$APP_NAME-$BRANCH_NAME".toLowerCase()
              HELM_RELEASE = "$PREVIEW_NAMESPACE".toLowerCase()
            }
            steps {
              container('python') {
                sh "pip install -r requirements.txt"
                sh "python -m pytest tests/test_rsvpapp.py"
                sh "export VERSION=$PREVIEW_VERSION && skaffold build -f skaffold.yaml"
                sh "jx step post build --image $DOCKER_REGISTRY/$ORG/$APP_NAME:$PREVIEW_VERSION"
                dir('./charts/preview') {
                  sh "make preview"
                  sh "jx preview --app $APP_NAME --dir ../.."
                }
              }
            }
          }
          stage('Build Release') {
            when {
              branch 'master'
            }
            steps {
              container('python') {
      
                // ensure we're not on a detached head
                sh "git checkout master"
                sh "git config --global credential.helper store"
                sh "jx step git credentials"
      
                // so we can retrieve the version in later steps
                sh "echo $(jx-release-version) > VERSION"
                sh "jx step tag --version $(cat VERSION)"
                sh "pip install -r requirements.txt"
                sh "python -m pytest tests/test_rsvpapp.py"
                sh "export VERSION=`cat VERSION` && skaffold build -f skaffold.yaml"
                sh "jx step post build --image $DOCKER_REGISTRY/$ORG/$APP_NAME:$(cat VERSION)"
              }
            }
          }
      . . .
      

      With the added lines, the Jenkins X pipeline will install dependencies and carry out a Python test whenever you commit a change to your application.

      Now that you have changed the sample RSVP application, commit and push these changes to GitHub with the following commands:

      • git add *
      • git commit -m update
      • git push

      When you push these changes to GitHub, you will trigger a new build of your application. If you open the Jenkins UI by navigating to http://jenkins.jx.your_IP_address.nip.io and entering "admin" for your username and password, you will find information about your new build. If you click "Build History" from the menu on the left side of the page, you should see a history of your committed builds. If you click on the blue icon next to a build then select "Console Ouput" from the lefthand menu, you will find the console output for the automated steps in your pipeline. Scrolling to the end of this output, you will find the following message:

      Output

      . . . Finished: SUCCESS

      This means that your application has passed your customized tests and is now successfully deployed.

      Once Jenkins X builds the application release, it will promote the application to the staging environment. To verify that your application is running, list the applications running on your Kubernetes cluster by using the following command:

      You will receive output similar to the following:

      Output

      APPLICATION STAGING PODS URL rsvpapp 0.0.2 1/1 http://rsvpapp.jx-staging.your_IP_address.nip.io

      From this, you can see that Jenkins X has deployed your application in your jx-staging environment as version 0.0.2. The output also shows the URL that you can use to access your application. Visiting this URL will show you the sample RSVP application:

      Sample RSVP Application in the Staging Environment

      Next, check out the activity of your application with the following command:

      • jx get activity -f rsvpapp

      You will receive output similar to the following:

      Output

      STEP STARTED AGO DURATION STATUS your_GitHub_username/rsvpappv/master #1 3h42m23s 4m51s Succeeded Version: 0.0.1 Checkout Source 3h41m52s 6s Succeeded CI Build and push snapshot 3h41m46s NotExecuted Build Release 3h41m46s 56s Succeeded Promote to Environments 3h40m50s 3m17s Succeeded Promote: staging 3h40m29s 2m36s Succeeded PullRequest 3h40m29s 1m16s Succeeded PullRequest: https://github.com/your_GitHub_username/environment-horsehelix-staging/pull/1 Merge SHA: dc33d3747abdacd2524e8c22f0b5fbb2ac3f6fc7 Update 3h39m13s 1m20s Succeeded Status: Success at: http://jenkins.jx.your_IP_address.nip.io/job/your_GitHub_username/job/environment-horsehelix-staging/job/master/2/display/redirect Promoted 3h39m13s 1m20s Succeeded Application is at: http://rsvpapp.jx-staging.your_IP_address.nip.io Clean up 3h37m33s 1s Succeeded your_GitHub_username/rsvpappv/master #2 28m37s 5m57s Succeeded Version: 0.0.2 Checkout Source 28m18s 4s Succeeded CI Build and push snapshot 28m14s NotExecuted Build Release 28m14s 56s Succeeded Promote to Environments 27m18s 4m38s Succeeded Promote: staging 26m53s 4m0s Succeeded PullRequest 26m53s 1m4s Succeeded PullRequest: https://github.com/your_GitHub_username/environment-horsehelix-staging/pull/2 Merge SHA: 976bd5ad4172cf9fd79f0c6515f5006553ac6611 Update 25m49s 2m56s Succeeded Status: Success at: http://jenkins.jx.your_IP_address.nip.io/job/your_GitHub_username/job/environment-horsehelix-staging/job/master/3/display/redirect Promoted 25m49s 2m56s Succeeded Application is at: http://rsvpapp.jx-staging.your_IP_address.nip.io Clean up 22m40s 0s Succeeded

      Here you are getting the Jenkins X activity for the RSVP application by applying a filter with -f rsvpapp.

      Next, list the pods running in the jx-staging namespace with the following command:

      • kubectl get pod -n jx-staging

      You will receive output similar to the following:

      NAME                                 READY     STATUS    RESTARTS   AGE
      jx-staging-mongodb-replicaset-0      1/1       Running   0          6m
      jx-staging-mongodb-replicaset-1      1/1       Running   0          6m
      jx-staging-mongodb-replicaset-2      1/1       Running   0          5m
      jx-staging-rsvpapp-c864c4844-4fw5z   1/1       Running   0          6m
      

      This output shows that your application is running in the jx-staging namespace, along with three pods of the backend MongoDB database, adhering to the changes you made to the YAML files earlier.

      Now that you have run a test application through the Jenkins X pipeline, you can try out promoting this application to the production environment.

      To finish up this demonstration, you will complete the CI/CD process by promoting the sample RSVP application to your jx-production namespace.

      First, use jx promote in the following command:

      • jx promote rsvpapp --version=0.0.2 --env=production

      This will promote the rsvpapp application running with version=0.0.2 to the production environment. Throughout the build process, Jenkins X will prompt you to enter your GitHub account information. Answer these prompts with your individual responses as they appear.

      After successful promotion, check the list of applications:

      You will receive output similar to the following:

      Output

      APPLICATION STAGING PODS URL PRODUCTION PODS URL rsvpapp 0.0.2 1/1 http://rsvpapp.jx-staging.your_IP_address.nip.io 0.0.2 1/1 http://rsvpapp.jx-production.your_IP_address.nip.io

      With this PRODUCTION information, you can confirm that Jenkins X has promoted rsvpapp to the production environment. For further verification, visit the production URL http://rsvpapp.jx-production.your_IP_address.nip.io in your browser. You should see the working application, now runnning from "production":

      Sample RSVP Application in the Production Environment

      Finally, list your pods in the jx-production namespace.

      • kubectl get pod -n jx-production

      You will find that rsvpapp and the MongoDB backend pods are running in this namespace:

      NAME                                     READY     STATUS    RESTARTS   AGE
      jx-production-mongodb-replicaset-0       1/1       Running   0          1m
      jx-production-mongodb-replicaset-1       1/1       Running   0          1m
      jx-production-mongodb-replicaset-2       1/1       Running   0          55s
      jx-production-rsvpapp-54748d68bd-zjgv7   1/1       Running   0          1m 
      

      This shows that you have successfully promoted the RSVP sample application to your production environment, simulating the production-ready deployment of an application at the end of a CI/CD pipeline.

      Conclusion

      In this tutorial, you used Helm to manage packages on a simulated Kubernetes cluster and customized a Helm chart to package and deploy your own application. You also set up a Jenkins X environment on your Kubernetes cluster and run a sample application through a CI/CD pipeline from start to finish.

      You now have experience with these tools that you can use when building a CI/CD system on your own Kubernetes cluster. If you'd like to learn more about Helm, check out our An Introduction to Helm, the Package Manager for Kubernetes and How To Install Software on Kubernetes Clusters with the Helm Package Manager articles. To explore further CI/CD tools on Kubernetes, you can read about the Istio service mesh in the next tutorial in this webinar series.



      Source link

      How To Build a Weather App with Angular, Bootstrap, and the APIXU API


      The author selected NPower to receive a donation as part of the Write for DOnations program.

      Introduction

      Angular is a front-end web framework built by Google. It allows developers to build single-page applications modeled around a model-view-controller (MVC) or model-view-viewmodel (MVVM) software architectural pattern. This architecture divides applications into different, but connected parts allowing for parallel development. Following this pattern, Angular splits its different components into the respective parts of a web application. Its components manage the data and logic that pertain to that component, display the data in its respective view, and adapts or controls the view based on the different messages that it receives from the rest of the app.

      Bootstrap is a front-end library that helps developers build responsive websites (sites that adapt to different devices), quickly and effectively. It makes use of a grid system that divides each page into twelve columns, which ensures that the page maintains its correct size and scale no matter what device it’s being viewed on.

      APIXU provides global weather data to users via their API. Using APIXU, a user can retrieve the latest weather as well as future weather forecasts for any location in the world.

      In this tutorial, you’ll create a weather app using Angular, Bootstrap, and the APIXU API. You’ll be able to type a location into a search form and on submission of that form, see the current weather details for that location displayed in your app. The Angular version used in this tutorial is 7.2.0 and the Bootstrap version used is 4.2.1.

      Prerequisites

      Before you begin this tutorial, you’ll need the following:

      Step 1 — Installing Angular

      Before you begin creating your app, you need to install Angular. Open your terminal and run the following command to install the Angular CLI globally on your machine:

      The Angular CLI is the Command Line Interface for Angular. It serves as the main way to create a new Angular project as well as the different sub-elements that make up an Angular project. Using the -g argument will install it globally.

      After a short while, you'll see the following output:

      Output from installing Angular

      ...
      + @angular/cli@7.2.2
      ...
      

      You've now installed Angular on your local machine. Next, you'll create your Angular application.

      Step 2 — Creating Your Angular App

      In this step you'll create and configure your new Angular application, install all its dependencies, such as Bootstrap and jQuery, and then finally check that the default application is working as expected.

      First, use the ng command to create an Angular application, you can run this from your terminal.

      Note: If you're on Windows, you may have issues trying to run an ng command from Command Prompt even though you've installed Node.js and npm correctly. For example, you may get an error such as: ng is not recognized as an internal or external command. In order to resolve this, please run the ng command inside the installed Node.js command prompt located in the Node.js folder on Windows.

      The ng command is a prerequisite to running any action with Angular from the command line. For example, whether you're building a new project, creating components, or creating tests, you prefix each desired functionality with the ng command. In this tutorial, you'll want to create a new application; you'll achieve this by executing the ng new command. The ng new command creates a new Angular application, imports the necessary libraries, and creates all the default code scaffolding that your application requires.

      Begin by creating a new application, in this tutorial it will be called weather-app, but you can change the name as you wish:

      The ng new command will prompt you for additional information about features that you want to add to your new application.

      Output

      Would you like to add Angular routing? (y/N)

      The Angular routing allows you to build single page applications with different views using the routes and components. Go ahead and type y or hit ENTER to accept the defaults.

      Output

      Which stylesheet format would you like to use? (Use arrow keys)

      Hit ENTER to accept the default CSS option.

      The app will continue its creation process, and after a short time you'll see the following message:

      Output

      ... CREATE weather-app/e2e/src/app.e2e-spec.ts (623 bytes) CREATE weather-app/e2e/src/app.po.ts (204 bytes) ... Successfully initialized git.

      Next, in your text editor, open the weather-app folder.

      Looking at the structure of your directory, you'll see several different folders and files. You can read a full explanation of what all of these files do here, but for the purposes of this tutorial, these are the most important files to understand:

      • The package.json file. Located in the root weather-app folder, it performs just like any other Node.js application, holding all the libraries your application will use, the name of your application, commands to run when testing, and so on. Primarily, this file holds details about external libraries that your Angular application needs in order to run properly.

      • The app.module.ts file. Located in the app folder within the weather-app/src folder, this file tells Angular how to assemble your application and holds details about the components, modules, and providers in your application. You'll already have an imported module, BrowserModule, within your imports array. The BrowserModule provides essential services and directives for your application and should always be the first imported module in your imports array.

      • The angular.json file. Located in the root weather-app folder of your app, this is the configuration file for the Angular CLI. This file holds internal configuration settings of what your Angular application needs to run. It sets defaults for your entire application, and has options such as what configuration files to use when testing, what global styles to use in your app, or to which folder to output your build files. You can find out more about these options in the official Angular-CLI documentation.

      You can leave all of these files alone for the moment, as you'll install Bootstrap next.

      Bootstrap has two dependencies that you'll need to install in order for it to work properly in Angular — jQuery and popper.js. jQuery is a JavaScript library focused on client-side scripting, while popper.js is a positioning library that mainly manages tooltips and popovers.

      In your terminal, move to your root weather-app directory:

      Then execute the following command to install all of the dependencies and save the references to the package.json file:

      • npm install --save jquery popper.js bootstrap

      The --save option automatically imports your references into the package.json file so that you don't have to manually add them after installation.

      You'll see output showing the version numbers that were installed, like the following:

      Output

      + [email protected] + [email protected] + [email protected] ...

      You have now successfully installed Bootstrap and its dependencies. However, you'll also need to include these libraries inside your application. Your weather-app does not yet know that it'll need these libraries, therefore you need to add the paths to jquery, popper.js, bootstrap.js, and bootstrap.css into your angular.json file.

      For popper.js, the file you'll need to include is node_modules/popper.js/dist/umd/popper.js. jQuery requires the node_modules/jquery/dist/jquery.slim.js file. Finally, for Bootstrap you'll need two files (both the JavaScript file and the CSS file). These are node_modules/bootstrap/dist/js/bootstrap.js and node_modules/bootstrap/dist/css/bootstrap.css respectively.

      Now that you have all the required file paths, open the angular.json file in your text editor. The styles array is where you'll add the reference to the CSS files, whilst the scripts array will reference all the scripts. You'll find both of these arrays near the top of the angular.json file, within the "options": JSON object. Add the following highlighted content to the file:

      angular.json

      ...
      "options:" {
      ...
      "styles": [
          "node_modules/bootstrap/dist/css/bootstrap.css",
           "src/styles.css"
      ],
      "scripts": [
          "node_modules/jquery/dist/jquery.slim.js",
          "node_modules/popper.js/dist/umd/popper.js",
          "node_modules/bootstrap/dist/js/bootstrap.js"
      ]},
      ...
      

      You've now imported the main .js and .css files you need for Bootstrap to work properly. You've specified the relative paths to these files from your angular.json file: adding your .css files in the styles array and .js files in the scripts array of angular.json. Make sure you've saved the angular.json file after adding this content.

      Now, start your application with the ng serve command to check that everything is working correctly. From the weather-app directory in your terminal, run:

      The --o argument will automatically open up a browser window that will show your application. The application will take a few seconds to build, and then will display in your browser.

      You'll see the following output in your terminal:

      Output

      ** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ ** ...

      Once the browser opens, you'll see a default Angular app page.

      Image of default created app in Angular

      If you don't see these outputs, run through this step again and ensure that everything is correct. If you see an error such as: Port 4200 is already in use. Use '--port' to specify a different port then you can change the port number by typing:

      • ng serve --o --port <different-port-number>

      The reason for this potential error message is because port 4200 on your machine is being used by another program or process. You can either, if you know what that process is, terminate it or you can follow the above step to specify a different port number.

      You've now set up your application scaffolding. Next, you'll create a weather component that will contain the main form and associated weather details of the search location.

      Step 3 — Creating Your Weather Component

      An Angular application is primarily made up of components, which are pieces of logic that have a particular function within an application. The component is composed of some logic that manages part of the screen in an application — this is called the view.

      For example in this tutorial, you're going to create a Weather Component that will be responsible for handling two tasks:

      • Searching for a location
      • Displaying associated weather data for that location

      To achieve the first objective, you'll create a form that will allow you to search for a location. When you click the search button on your form, it will trigger a function that will search for that location.

      To achieve the second objective, you'll have a <div> with nested <p> tags that will neatly display your retrieved data.

      Whilst your app is running from your terminal window, you can't type anything else in that particular window. Therefore, open up the weather-app directory in a new terminal window if you want to execute other ng commands. Alternatively, you can stop the app from running in the original terminal window by pressing CTRL + C. You can then install the new component, and after that start the app again by typing ng serve --o.

      Execute the following command that will create your Weather Component and automatically import it into your app.module.ts file. Remember that your app.module.ts file holds details about all the components, modules, and providers in your application.

      • ng generate component weather

      You'll see output like this (the exact byte sizes may vary):

      Output

      CREATE src/app/weather/weather.component.css (0 bytes) CREATE src/app/weather/weather.component.html (26 bytes) CREATE src/app/weather/weather.component.spec.ts (635bytes) CREATE src/app/weather/weather.component.ts (273 bytes) UPDATE src/app/app.module.ts (400 bytes) ...

      This output shows that Angular has created the four files necessary for a component:

      • The .css and .html files for your view
      • A .spec.ts file for testing your component
      • A.component.ts file to hold your component's functions

      Angular has also updated the src/app/app.module.ts file to add a reference to the newly created component. You'll always find component files under the src/app/name-of-component directory.

      Now that you have installed your new component, return to your browser to see the app. If you stopped the app running to install the new component, start it again by typing:

      You'll notice that you can still see "Welcome to app!" (the default component) displayed on the page. You can't see your newly created component. In the next section, you'll change this so that whenever you go to localhost:4200, you'll access your newly created weather component instead of Angular's default component.

      Step 4 — Accessing Your Weather Component

      In standard HTML, whenever you want to create a new page, you create a new .html file. For example, if you already had a pre-existing HTML page from which you wanted to navigate to your newly created page, you'd have an href attribute with an anchor tag to point to that new page. For example:

      preexisting.html

      <a href="http://www.digitalocean.com/newpage.html">Go to New Page</a>
      

      In Angular, however, this works slightly differently. You cannot use an href attribute in this way to navigate to a new component. When you want to link through to a component, you need to make use of Angular's Router library and declare a desired URL path within a file that will map directly to a component.

      In Angular, you call this file routes.ts. This holds all the details of your routes (links). For this file to work correctly, you will import the Routes type from the @angular/router library and list your desired links to be of type Routes. This will communicate to Angular that these are a list of routes for navigation in your app.

      Create the file routes.ts in your text editor and save it in the src/app directory. Next, add the following content to the routes.ts file:

      src/app/routes.ts

      import { Routes } from '@angular/router'
      

      Now, declare the URL path and the component in src/app/routes.ts. You want to make your app such that when you go to the homepage (http://localhost:4200), you access your newly created Weather Component. Add these lines to the file, which will map the root URL to the Weather Component you just created:

      src/app/routes.ts

      import { Routes } from '@angular/router'
      import { WeatherComponent } from './weather/weather.component';
      
      export const allAppRoutes: Routes = [
        { path: '', component: WeatherComponent }
      ];
      

      You've imported your WeatherComponent, and then created a variable allAppRoutes that's an array of type Routes. The allAppRoutes array holds route definition objects each containing a URL path and the component to map to. You've specified that any time you go to the root URL (''), it should navigate to the WeatherComponent.

      Your final routes.ts file will look like this:

      src/app/routes.ts

      import { Routes } from "@angular/router";
      import { WeatherComponent } from "./weather/weather.component";
      
      export const allAppRoutes: Routes = [
        { path: '', component: WeatherComponent }
      ];
      

      You now need to add these routes to your main app.module.ts file. You need to pass the array you just created — allAppRoutes — into an Angular module called the RouterModule. The RouterModule will initialize and configure the Router (responsible for carrying out all app navigation) and provide it with its routing data from allAppRoutes. Add the following highlighted content:

      src/app/app.module.ts

      ...
      import {WeatherComponent} from './weather/weather.component';
      import {RouterModule} from '@angular/router';
      import {allAppRoutes} from './routes';
      ...
      @NgModule({
          declarations:[
            ...
          ],
          imports: [
              BrowserModule,
              RouterModule.forRoot(allAppRoutes)
          ]
          ...
      })
      ...
      

      In this file, you've imported the RouterModule and allAppRoutes array of route objects. You've then passed the allAppRoutes array into the RouterModule so that your Router knows where to route your URLs to.

      Lastly, you need to enable routing itself. Open the app.component.ts file. There's a templateUrl property that specifies the HTML for that particular component: ./app.component.html. Open this file, src/app/app.component.html, and you will see that it contains all of the HTML for your localhost:4200 page.

      Remove all of the HTML contained within app.component.html and replace it with:

      src/app/app.component.html

      <router-outlet></router-outlet>
      

      The router-outlet tag activates routing and matches the URL the user types into the browser to the route definition you created earlier in the routes.ts file under the allAppRoutes variable. The router then displays the view in the HTML. In this tutorial, you'll display the weather.component.html code directly after the <router-outlet></router-outlet> tag.

      Now, if you navigate to http://localhost:4200, you will see weather works! appear on your page.

      You've set up routing in your application. Next, you'll create your form and details section that will enable you to search for a location and show its associated details.

      Step 5 — Defining the User Interface

      You'll be using Bootstrap to act as the scaffolding for your application view. Bootstrap is useful for creating ready-made, responsive websites that adapt to any device (mobile, tablet, or desktop). It achieves this by treating every row on a webpage as twelve columns wide. On a webpage, a row is simply a line from one end of the page to the other. This means that every page's content must be contained within that line, and it must equal twelve columns. If it doesn't equal twelve columns, it'll be pushed down to another row. For example, in Bootstrap's grid system, there would be a twelve-column row divided into two sections of six columns, and the next twelve-column row divided into three sections of four columns.

      In the Bootstrap documentation, you can read more about this grid system.

      You'll be splitting your page into two sections of six columns with the left column holding your search form and the right showing the weather details.

      Open src/app/weather/weather.component.html to access your WeatherComponent HTML code. Delete the paragraph that is currently in the file, and then add the following code:

      src/app/weather/weather.component.html

      <div class="container">
        <div class="row">
          <div class="col-md-6"><h3 class="text-center">Search for Weather:</h3></div>
          <div class="col-md-6"><h3 class="text-center">Weather Details:</h3></div>
        </div>
      </div>
      
      

      You created a <div> with class container to hold all your content. You then created a row that you split into two sections of six columns each. The left-hand side will hold your search form and the right, your weather data.

      Next, to build your form, you'll work in the first col-md-6 column. You'll also add a button that will submit what you've typed into your form to APIXU, which will then return the requested weather details. To do this, identify the first col-md-6 class and add the following highlighted content underneath the <h3> tag:

      src/app/weather/weather.component.html

      ...
      <div class="col-md-6">
        <h3 class="text-center">Search for Weather:</h3>
        <form>
          <div class="form-group">
            <input
              class="form-control"
              type="text"
              id="weatherLocation"
              aria-describedby="weatherLocation"
              placeholder="Please input a Location"
            />
           </div>
           <div class="text-center"> 
            <button type="submit" class="btn btn-success btn-md">
              Search for the weather</button>
           </div>
         </form> 
      </div>
      ...
      

      You've added your form and added a form-group class that holds your search bar. You've also created your button to search for the weather. In your browser, your weather app page will look like this:

      Image of weather app page so far

      This looks a little compact, so you can add some CSS in order to style the page with some better spacing. The major advantage of Bootstrap is that it comes with spacing classes that you can add to your HTML without needing to write any extra CSS of your own. If, however, there is any extra CSS you would like to incorporate that Bootstrap's standard classes don't cover, you can write in your own CSS as necessary. For this tutorial, you will use Bootstrap's standard classes.

      For every <h3> tag, you will add the .my-4 Boostrap CSS class. The m sets margin on the element, the y sets both margin-top and margin-bottom on the element, and finally 4 specifies the amount of margin to add. You can find out more details about the different spacing types and sizes here. In your weather.component.html file, add the following highlighted content to replace the current <h3> tags:

      src/app/weather/weather.component.html

      <div class="col-md-6">
        <h3 class="text-center my-4">Search for Weather:</h3>
        <form>
          <div class="form-group">
            <input
              class="form-control"
              type="text"
              id="weatherLocation"
              aria-describedby="weatherLocation"
              placeholder="Please input a Location"
            />
          </div>
          <div class="text-center">
            <button type="submit" class="btn btn-success btn-md">
              Search for the weather
            </button>
          </div>
        </form>
      </div>
      <div class="col-md-6">
        <h3 class="text-center my-4">Weather Details:</h3>
      </div>
      

      Reload the page in your browser and you'll see that you have more spacing.

      Image of spacing applied to weather app

      You've created your form as well as the section where you're going to display the information you receive from the APIXU API. Next, you'll wire up your form to be able to input your location correctly.

      Step 6 — Wiring Up Your Form

      In Angular, there are two ways of creating forms for user input in your application — reactive or template-driven. Although they achieve the same result, each form type handles processing user input data differently.

      With reactive forms, you create a list of the different elements of your form in your .component.ts file. You then connect them to your created HTML form within the respective .component.html file. This is strictly one-way; that is, data flows from your HTML to your .component.ts file, there is no bi-directional flow of data.

      With template-driven forms, you create your form as you would in normal HTML. Then, using directives such as ngModel, you can create either one-way or two-way data bindings from your HTML, back to your data model in your component, and vice-versa.

      There are strengths and weaknesses in each approach, but in general, reactive forms are preferable because of the:

      • Flexibility to create forms of varying complexities.
      • Simplicity to unit test by checking on the state of each form control in the component's .component.ts file.
      • Capability to subscribe to values within a form. A developer can subscribe to the form's value stream allowing them to perform some action on values being typed into the form in real time.

      Despite these strengths, reactive forms can sometimes be more complex to implement. This can lead to developers writing more code than compared to a template-driven form. To see a comprehensive overview of both form types and best use cases, Angular's official guide provides a good starting point. For this tutorial, you'll be using reactive forms.

      To use a reactive form, open the file app.module.ts. Next, import the ReactiveFormsModule by declaring the import toward the top of the file.

      src/app/app.module.ts

      ...
      import { ReactiveFormsModule } from '@angular/forms';
      @NgModule({
          ...
      })
      ...
      

      Finally, add the ReactiveFormsModule to your list of imports.

      src/app/app.module.ts

      ...
      @NgModule({
          ...
          imports: [
              BrowserModule,
              RouterModule.forRoot(allAppRoutes),
              ReactiveFormsModule
          ]
          ...
      })
      ...
      

      Following these code additions, your app.module.ts will look like this:

      src/app/app.module.ts

      import { BrowserModule } from "@angular/platform-browser";
      import { NgModule } from "@angular/core";
      
      import { AppComponent } from "./app.component";
      import { WeatherComponent } from "./weather/weather.component";
      import { RouterModule } from "@angular/router";
      import { allAppRoutes } from "./routes";
      import { ReactiveFormsModule } from "@angular/forms";
      
      @NgModule({
        declarations: [AppComponent, WeatherComponent],
        imports: [
          BrowserModule,
          RouterModule.forRoot(allAppRoutes),
          ReactiveFormsModule
        ],
        providers: [],
        bootstrap: [AppComponent]
      })
      export class AppModule {}
      

      Once you've added both of these lines, open the weather.component.ts file and import the FormBuilder and FormGroup classes.

      src/app/weather/weather.component.ts

      import { Component, OnInit } from '@angular/core';
      import { FormBuilder, FormGroup } from '@angular/forms';
      

      Now create a variable in your weather.component.ts file that will reference your FormGroup:

      weather.component.ts

      export class WeatherComponent implements OnInit {
         public weatherSearchForm: FormGroup;
         constructor() { }
      ...
      

      Every time you want to perform an action on your form, you'll reference it via the weatherSearchForm variable. You'll now add the FormBuilder import into your constructor so that you can use it in your component.

      weather.component.ts

      ...
      public weatherSearchForm: FormGroup;
      constructor(private formBuilder: FormBuilder) {}
      ...
      

      By adding the formBuilder to the constructor, it creates an instance of the FormBuilder class, allowing you to use it within your component.

      You are now ready to create your FormGroup and its respective values in the weather.component.ts file. If you have several input options in your form, it's best practice to enclose it within a FormGroup. In this tutorial, you will only have one (your location input), but you will use the FormGroup anyway for practice.

      It's important that your form is ready for use when you navigate to your component. Because you're using a reactive form, you must create the tree of elements within the form first before you bind it to the HTML. To achieve this, you need to ensure that you create your form elements in the ngOnInit hook inside your WeatherComponent. The ngOnInit method runs once at the initialization of a component, executing any logic that you specify needs to run before the component is ready to use.

      You therefore have to create your form before you can complete the binding to HTML process.

      In your WeatherComponent, you'll initialize the form within the ngOnInit hook:

      src/app/weather/weather.component.ts

      ...
      constructor(private formBuilder: FormBuilder) {}
      ngOnInit() {
          this.weatherSearchForm = this.formBuilder.group({
            location: ['']
          });
        }
      

      You have created the first part of the form according to reactive form style: defining your form components in the weather.component.ts file. You've created a group of your form's composite elements (at the moment, you have one element, location). The [''] array allows you to specify some extra options for your form inputs such as: pre-populating it with some data and using validators to validate your input. You have no need of any of these for this tutorial, so you can just leave it blank. You can find out more about what you can pass into an element property here.

      You have two more things to do before your form is complete. First open up your weather.component.html file. You need to assign the form a property [formGroup]. This property will be equal to the variable you just declared in your weather.component.ts file: weatherSearchForm. Second, you have to bind your location element (declared in your weather.component.ts file) to your HTML. In weather.component.html, add the following highlighted content:

      src/app/weather/weather.component.html

      ...
      <form
        [formGroup]="weatherSearchForm" >
        <div class="form-group">
          <input
            class="form-control"
            type="text"
            id="weatherLocation"
            aria-describedby="weatherLocation"
            placeholder="Please input a Location"
          />formControlName="location" />
        </div>
        <div class="text-center">
          <button type="submit" class="btn btn-success btn-md">
            Search for the weather
          </button>
        </div>
      </form>
      ...
      

      You've added the [formGroup] property, binding your form to HTML. You've also added the formControlName property that declares that this particular input element is bound to the location element in your weather.component.ts file.

      Save your file and return to your browser, you'll see that your app looks exactly the same. This means that your form is correctly wired up. If you see any errors at this stage, then please go back through the previous steps to ensure that everything is correct in your files.

      Next, you'll wire up your button to be able to accept input data into your form.

      Step 7 — Connecting Your Button

      In this step you're going to connect your search button to your form in order to be able to accept the user's input data. You're also going to create the scaffolding for the method that will eventually send the user's input data to the APIXU weather API.

      If you take a look back at your code in weather.component.html, you can see that your button has a type submit:

      src/app/weather/weather.component.html

      <form>
      ...
      <div class="text-center">
          <button type="submit" class="btn btn-success btn-md">Search for the weather</button>
      </div>
      </form>
      

      This is a standard HTML value that will submit your form values to some function to take action on.

      In Angular, you specify that function in the (ngSubmit) event. When you click your button in your form, as long as it has a type of submit, it will trigger the (ngSubmit) event, which will subsequently call whatever method you have assigned to it. In this case, you want to be able to get the location that your user has typed in and send it to the APIXU API.

      You're going to first create a method to handle this. In your weather.component.ts, create a method sendToAPIXU() that will take one argument: the value(s) you've typed into your form. Add the following highlighted content to the file:

      src/app/weather/weather.component.ts

      ...
      ngOnInit() {
          this.weatherSearchForm = this.formBuilder.group({
            location: [""]
          });
        }
      
      sendToAPIXU(formValues) {
      
      }
      ...
      

      Next, add the ngSubmit event to your HTML and pass the values of your submitted form into the sendToAPIXU() method:

      weather.component.html

      ...
      <form [formGroup]="weatherSearchForm" (ngSubmit)="sendToAPIXU(weatherSearchForm.value)">
        ...
      </form>
      ...
      

      You've added the ngSubmit event to your form, connected your method you want to run when you submit your form, and passed in the values of your weatherSearchForm as an argument to your handler method (weatherSearchForm.value). You can now test this works by using console.log to print out your formValues, in your sendToAPIXU() method, add the following highlighted content to weather.component.ts:

      weather.component.ts

      ...
      sendToAPIXU(formValues){
          console.log(formValues);
      }
      

      Go to your browser and open your console by right clicking anywhere on your website page, and then click on Inspect Element. There will be a tab on the window that pops up called Console. Type London into your form. When you click on the Search for Weather button, you'll see an object with your location enclosed.

      Output from console after updating the sendToAPIXU method

      Your output from the console is a JSON object {location: "London"}. If you wanted to access your location value, you can do this by accessing formValues.location. Similarly, if you had any other inputs inside your form, you would swap .location for any other element names you had.

      Note:
      All values of a reactive form are stored in an object — where the key is the name of the value you passed into the formBuilder.group({}).

      The button is now wired up and can receive input correctly. Next, you'll make the sendToAPIXU() method make an HTTP request to the APIXU API.

      Step 8 — Calling the APIXU API

      The APIXU API accepts location information, searches the current weather details for that location, and returns them back to the client. You'll now modify your app so that it sends location data to the API, obtains the response, and then displays the results on your page.

      In order to make HTTP requests in Angular, you have to import the HttpClientModule. Open your src/app/app.module.ts and add the following highlighted lines:

      src/app/app.module.ts

      ...
      import { ReactiveFormsModule } from '@angular/forms';
      import { HttpClientModule } from '@angular/common/http';
      @NgModule({
          ...
          imports: [
              BrowserModule,
              RouterModule.forRoot(allAppRoutes),
              ReactiveFormsModule,
              HttpClientModule
          ]
          ...
      })
      ...
      

      Next, you need to write the code to make the HTTP call to the APIXU API. It's best practice to create an Angular service to make HTTP requests. Separation of concerns is key in any app that you build. A service allows you to move all of those HTTP requests your app makes into one file that you can then call inside any .component.ts file you create. You could "legally" write in those HTTP requests in the specific .component.ts file, but this isn't best practice. You may, for instance, find that some of your requests are complex and require you to perform some post-processing actions after receiving your data. Several different components in your app might use some of your HTTP requests, and you don't want to write the same method multiple times.

      From a new terminal window or by stopping the server in your current terminal session, execute the following command to create a service called apixu:

      You'll see output resembling the following:

      Output

      create src/app/apixu.service.spec.ts (328 bytes) create src/app/apixu.service.ts (134 bytes) ...

      The command created the service file (apixu.service.ts) and a test file (apixu.service.spec.ts).

      You now need to add this service as a provider into your app.module.ts file. This makes it available to use inside your app. Open this file, and first import the ApixuService:

      src/app/app.module.ts

      ...
      import { HttpClientModule } "@angular/common/http";
      import { ApixuService } from "./apixu.service";
      ...
      

      Next add the newly imported ApixuService as a provider into the providers block:

      src/app/app.module.ts file

      ...
      @NgModule({
          ...
          providers: [ApixuService],
          ...
      })
      ...
      

      In Angular, if you want to use a service that you have created, you need to specify that service as a provider within your module.ts file. In this case, you've specified it as a provider within your entire application in app.module.ts.

      Finally, open up the src/app/apixu.service.ts file. You'll see the boilerplate code of what you need to create a service: first the import of the Injectable interface from Angular; then the fact that the service should be with the providedIn root injector (for the entire application); and then the decorating (this effectively means specifying) of your service as @Injectable.

      src/app/apixu.service.ts

      import { Injectable } from '@angular/core';
      
      @Injectable({
        providedIn: 'root'
      })
      export class ApixuService {
      
        constructor() { }
      }
      

      The decorating of the service as @Injectable allows you to inject this service within the constructor in weather.component.ts so that you can use it inside your component.

      If you stopped your application, restart it by running:

      As aforementioned, your service needs to make HTTP requests to the APIXU API and import the HttpClientModule in the app.module.ts file to make HTTP requests throughout the application. You additionally need to import the HttpClient library into the apixu.service.ts file to make HTTP requests to the APIXU API from the apixu.service.ts file itself. Open the apixu.service.ts file, and add the following highlighted content:

      src/app/apixu.service.ts

      ...
      import { HttpClient } from '@angular/common/http';
      ...
      

      Now you need to write a method, getWeather(), that takes in one paramater: location. This method will make an API request to APIXU and return the retrieved location data.

      For this, you'll need the provided API key when you signed up for the APIXU API. If you log in to APIXU, you'll come to the dashboard:

      APIXU Dashboard

      You will see your key, and below that, links to the API URL with your key already pre-filled for both the Current Weather and Forecast Weather. Copy the HTTPS link for the Current Weather details, it will be something like:

      https://api.apixu.com/v1/current.json?key=YOUR_API_KEY&q=Paris

      This URL will give you current weather details for Paris. You want to be able to to pass in the location from your form into the &q= parameter instead. Therefore, remove Paris from the URL as you add it to your apixu.service.ts file:

      src/app/apixu.service.ts

      ...
      export class ApixuService {
      
        constructor(private http: HttpClient) {}
      
        getWeather(location){
            return this.http.get(
                'https://api.apixu.com/v1/current.json?key=YOUR_API_KEY&q=' + location
            );
        }
      }
      

      Note: You've used the API key directly within the code. In a production situation, you should store this securely server-side, and retrieve this key in a secure manner and use it within your application. You can either store it securely server-side, or use a key management application such as Hashicorp Vault or Azure Key Vault, to name a few.

      You've now imported and injected HttpClient into the constructor so that you can use it. You've also created a method getWeather() that takes a location parameter and makes a GET request to your provided URL. You left the &q= parameter blank, as you're going to provide this location directly from location parameter in the method. Lastly, you've returned the data back to whoever called the method.

      Your service is now complete. You need to import your service into your WeatherComponent, inject it into your constructor to use it, and then update your sendToAPIXU() method to send your location to your newly created service. Open the weather.component.ts file to complete these tasks by adding the highlighted content:

      src/app/weather.component.ts

      ...
      import { FormBuilder, FormGroup } from "@angular/forms";
      import { ApixuService } from "../apixu.service";
      ...
      constructor(
          private formBuilder: FormBuilder,
          private apixuService: ApixuService
        ) {}
      ...
      ngOnInit(){...}
      sendToAPIXU(formValues){
          this.apixuService
            .getWeather(formValues.location)
            .subscribe(data => console.log(data));
      }
      

      You've removed the former console.log statement in your sendToAPIXU() method and updated it with this content. You're now passing in your location from your form to the sendToAPIXU() method you created earlier. You've then passed that data to the getWeather() method of the ApixuService that has subsequently made an HTTP request to the API with that location. You've then subscribed to the response you got back and, in this example, logged that data to the console. You always have to call the subscribe method on an HTTP request as the request will not begin until you have a way of reading the Observable response you get back. Observables are a way of sending messages between publishers and subscribers, allowing you to pass any kind of data back and forth. You will not be able to receive data from an observable until a subscriber has subscribed to it, because it won't execute before that point.

      Open the console in your browser again again. Now, type in London, UK and click Search for Weather. If you click on the tab arrows, you'll see a list of the weather details in the console.

      Console output from looking for current weather in London, UK

      The output shows JSON objects containing all of the weather information needed. You have two objects returned: a current object and a location object. The former gives the desired weather details and the latter details about your location.

      You've now got your weather data successfully showing in the console. To finish this tutorial, you'll display these weather details in your HTML.

      Step 9 — Displaying Your Weather Data in Your App

      Displaying the results in the console is a good initial step to check that everything is working. However, you want to eventually show the weather data in HTML for your users. To do this, you'll create a variable to hold your returned weather data, and then display that using interpolation in your HTML.

      Interpolation allows you to display data in your views. To do this, it requires you to bind a property via the {{ }} style, to show that property in your HTML.

      Open up the weather.component.ts file and create a variable called weatherData to which you'll assign the retrieved JSON data from the API. Additionally, remove the code that was previously in the .subscribe() brackets and replace it with the following highlighted code:

      src/app/weather/weather.component.ts

      ...
      export class WeatherComponent implements OnInit {
      public weatherSearchForm: FormGroup;
      public weatherData: any;
      ...
      sendToAPIXU(formValues){
          this.apixuService
          .getWeather(formValues.location)
          .subscribe(data => this.weatherData = data)
            console.log(this.weatherData);
          }
      }
      

      You've created the variable weatherData and declared that it can hold data of any type. You've then assigned the data you receive back from your API call to that variable. Finally, you've added a console.log() statement to double check that weatherData holds all of your retrieved information.

      Your weather.component.ts file should be looking like this at this stage:

      src/app/weather/weather.component.ts

      import { Component, OnInit } from "@angular/core";
      import { FormBuilder, FormGroup } from "@angular/forms";
      import { ApixuService } from "../apixu.service";
      
      @Component({
        selector: "app-weather",
        templateUrl: "./weather.component.html",
        styleUrls: ["./weather.component.css"]
      })
      export class WeatherComponent implements OnInit {
        public weatherSearchForm: FormGroup;
        public weatherData: any;
      
        constructor(
          private formBuilder: FormBuilder,
          private apixuService: ApixuService
        ) {}
      
        ngOnInit() {
          this.weatherSearchForm = this.formBuilder.group({
            location: [""]
          });
        }
      
        sendToAPIXU(formValues) {
          this.apixuService.getWeather(formValues.location).subscribe(data => {
            this.weatherData = data;
            console.log(this.weatherData);
          });
        }
      }
      

      If you go back and search for London, UK again, you'll see your object printed out to the console as normal. Now, you want to show this data in your HTML. If you examine the current object from the retrieved weather data in the console, you'll see values such as condition, feelslike_c, feelslike_f, temp_c, temp_f, and so on You're going to make use of all five of these properties.

      Open your weather.component.html file again and add in the subtitles to the data you want to display. You'll be adding these <p> tags within the second col-md-6:

      src/app/weather/weather.component.html

      ...
      <div class="col-md-6">
        <h3 class="text-center my-4">Weather Details:</h3>
        <p class="text-center">Current weather conditions:</p>
        <p class="text-center">Temperature in Degrees Celsius:</p>
        <p class="text-center">Temperature in Degrees Farenheit:</p>
        <p class="text-center">Feels like in Degrees Celsius:</p>
        <p class="text-center">Feels like in Degrees Farenheit:</p>
        <p class="text-center">Location Searched:</p>
      </div>
      

      Next, you'll add the data you have received from your JSON object to your HTML:

      weather.component.html

      ...
      <h3 class="text-center my-4 ">Weather Details:</h3>
      <p class="text-center">
        Current weather conditions: {{this.weatherData?.current.condition.text}}
      </p>
      <p class="text-center">
        Temperature in Degrees Celsius: {{this.weatherData?.current.temp_c}}
      </p>
      <p class="text-center">
        Temperature in Degrees Farenheit: {{this.weatherData?.current.temp_f}}
      </p>
      <p class="text-center">
        Feels like in Degrees Celsius: {{this.weatherData?.current.feelslike_c}}
      </p>
      <p class="text-center">
        Feels like in Degrees Farenheit:
        {{this.weatherData?.current.feelslike_f}}
      </p>
      <p class="text-center">
        Location Searched: {{this.weatherData?.location.name}},
        {{this.weatherData?.location.country}}
      </p>
      

      You have used an operator ? as you retrieved data from your weatherData variable within your HTML. This operator is called an Elvis Operator.

      Because you're making an HTTP call, you're making an asynchronous request. You'll get that data back at some point, but it will not be an immediate response. Angular, however, will still continue to fill out your HTML with the data you specified from the weatherData variable. If you haven't received data back by the time that Angular begins to populate your paragraphs, there will be an error stating that Angular can't find that data. For example, .current or .location would be showing as undefined.

      The Elvis Operator is a safe navigator and prevents this from happening. It tells Angular to wait and check if weatherData is first defined, before going ahead and showing that data in the HTML. Once weatherData has all of its information, Angular will then update your bindings and show your data as normal.

      You final weather.component.ts file will look like the following:

      weather.component.html

      <div class="container">
        <div class="row">
          <div class="col-md-6">
            <h3 class="text-center my-4">Search for Weather:</h3>
            <form
              [formGroup]="weatherSearchForm"
              (ngSubmit)="sendToAPIXU(weatherSearchForm.value)"
            >
              <div class="form-group">
                <input
                  class="form-control"
                  type="text"
                  id="weatherLocation"
                  aria-describedby="weatherLocation"
                  placeholder="Please input a Location"
                  formControlName="location"
                />
              </div>
              <div class="text-center">
                <button type="submit" class="btn btn-success btn-md">
                  Search for the weather
                </button>
              </div>
            </form>
          </div>
          <div class="col-md-6">
            <h3 class="text-center my-4">Weather Details:</h3>
            <p class="text-center">
              Current weather conditions: {{ this.weatherData?.current.condition.text
              }}.
            </p>
            <p class="text-center">
              Temperature in Degrees Celsius: {{ this.weatherData?.current.temp_c }}
            </p>
            <p class="text-center">
              Temperature in Degrees Farenheit: {{ this.weatherData?.current.temp_f }}
            </p>
            <p class="text-center">
              Feels like in Degrees Celsius: {{ this.weatherData?.current.feelslike_c
              }}
            </p>
            <p class="text-center">
              Feels like in Degrees Farenheit: {{
              this.weatherData?.current.feelslike_f }}
            </p>
            <p class="text-center">
              Location Searched: {{ this.weatherData?.location.name }}, {{
              this.weatherData?.location.country }}.
            </p>
          </div>
        </div>
      </div>
      

      You've followed the pattern of the returned JSON weather object in order to output your desired data. Save your file, go back to your browser, and type London, UK, you'll see your weather data appear on the right-hand side.

      Finished app showing weather data from London, UK

      Try it with different locations, like: San Francisco, US, Dakar, Senegal, and Honololu, Hawaii. You'll see the respective weather data appear for all those locations.

      Conclusion

      You have created a weather app using Angular, Bootstrap, and the APIXU API. You have set up an Angular project from scratch, following Angular best practices while ensuring your application is well designed and set up appropriately.

      Angular is an advanced framework allowing you to create anything from small web applications to large, complex ones with ease. Angular, as with any frameworks, does have a learning curve, but small projects like this one can help you to quickly learn and start using it productively.

      Another feature to consider adding to your application is handling errors from your HTTP requests; for instance, if you were to type in an invalid location. Another enhancement would be displaying different images if the temperature is between certain thresholds. You can also create different applications with Angular using other APIs.

      You may also want to use NgBootstrap, which is a special type of Bootstrap built for Angular. This allows you to use all the standard Bootstrap JavaScript widgets as well as some special ones not included in the standard installation specifically adapted for Angular.

      The full code for this tutorial is available on GitHub.



      Source link