From aea34c50153e7aa30dd98ca1f69944fa5f8207b4 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sun, 22 Mar 2020 13:05:08 -0500 Subject: [PATCH 01/93] Add link to GCP regions and zones Resolves issue #22. --- docs/01-prerequisites.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/01-prerequisites.md b/docs/01-prerequisites.md index b36ea60..a64687d 100644 --- a/docs/01-prerequisites.md +++ b/docs/01-prerequisites.md @@ -30,7 +30,7 @@ $ gcloud auth application-default login This tutorial assumes a default compute region and zone have been configured. -Set a default compute region: +Set a default compute region appropriate to your location ([GCP regions and zones](https://cloud.google.com/compute/docs/regions-zones)): ```bash $ gcloud config set compute/region europe-west1 @@ -48,4 +48,4 @@ Verify the configuration settings: $ gcloud config list ``` -Next: [Manual operations](02-manual-operations.md) \ No newline at end of file +Next: [Manual operations](02-manual-operations.md) From a24960b479c8f89fbff9783edc9a841a3c4e776d Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sun, 22 Mar 2020 13:11:10 -0500 Subject: [PATCH 02/93] minor adjustment --- docs/01-prerequisites.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/01-prerequisites.md b/docs/01-prerequisites.md index a64687d..a6bb3b1 100644 --- a/docs/01-prerequisites.md +++ b/docs/01-prerequisites.md @@ -36,7 +36,7 @@ Set a default compute region appropriate to your location ([GCP regions and zone $ gcloud config set compute/region europe-west1 ``` -Set a default compute zone: +Set a default compute zone appropriate to the zone: ```bash $ gcloud config set compute/zone europe-west1-b From 784485bf7f01615e5b82ed1fd0a920438e269751 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sun, 22 Mar 2020 13:19:16 -0500 Subject: [PATCH 03/93] Offending ECDSA key error fix --- docs/03-scripts.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/03-scripts.md b/docs/03-scripts.md index e53a828..f348bbc 100644 --- a/docs/03-scripts.md +++ b/docs/03-scripts.md @@ -110,6 +110,7 @@ Copy the `scripts` directory to the created VM: $ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe raddit-instance-3) $ scp -r ./scripts raddit-user@${INSTANCE_IP}:/home/raddit-user ``` +NOTE: If you get an `offending ECDSA key` error, use the suggested removal command. Connect to the VM via SSH: ```bash From 96d0c8dc216b3ccdff2f2cecf96e7422f8b51fef Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sun, 22 Mar 2020 13:31:45 -0500 Subject: [PATCH 04/93] allow unauthenticated mongodb install --- docs/02-manual-operations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/02-manual-operations.md b/docs/02-manual-operations.md index 318b291..ebefdbb 100644 --- a/docs/02-manual-operations.md +++ b/docs/02-manual-operations.md @@ -112,7 +112,7 @@ Install MongoDB which your application uses: $ sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv EA312927 $ echo "deb http://repo.mongodb.org/apt/ubuntu xenial/mongodb-org/3.2 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.2.list $ sudo apt-get update -$ sudo apt-get install -y mongodb-org +$ sudo apt-get install -y mongodb-org --allow-unauthenticated ``` Start MongoDB and enable autostart: From 5a774cc2c3ff143adc3c59341ca0c6ca42d11562 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sun, 22 Mar 2020 13:32:29 -0500 Subject: [PATCH 05/93] Update 03-scripts.md --- docs/03-scripts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/03-scripts.md b/docs/03-scripts.md index f348bbc..6d775e1 100644 --- a/docs/03-scripts.md +++ b/docs/03-scripts.md @@ -69,7 +69,7 @@ echo " ----- install mongodb ----- " apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv EA312927 echo "deb http://repo.mongodb.org/apt/ubuntu xenial/mongodb-org/3.2 multiverse" > /etc/apt/sources.list.d/mongodb-org-3.2.list apt-get update -apt-get install -y mongodb-org +apt-get install -y mongodb-org --allow-unauthenticated echo " ----- start mongodb ----- " systemctl start mongod From e873ba906250fe0926e6c78b0f757af970572dca Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Fri, 27 Mar 2020 19:45:49 -0500 Subject: [PATCH 06/93] Update 01-prerequisites.md --- docs/01-prerequisites.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/01-prerequisites.md b/docs/01-prerequisites.md index a6bb3b1..e09e680 100644 --- a/docs/01-prerequisites.md +++ b/docs/01-prerequisites.md @@ -4,6 +4,8 @@ In this tutorial, we use the [Google Cloud Platform](https://cloud.google.com/) to provision the compute infrastructure. You can [sign up](https://cloud.google.com/free/) for $300 in free credits, which will be more than sufficient to complete all of the labs in this tutorial. +NOTE: SEIS664 -- You do not need to install the Google Cloud SDK if you are using the Google Cloud Shell as recommended for SEIS664. It is already installed. + ## Google Cloud Platform SDK ### Install the Google Cloud SDK From 92b1d4c8110e747b16a6ad84b067c75a89a12a60 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Fri, 27 Mar 2020 19:47:01 -0500 Subject: [PATCH 07/93] Update 01-prerequisites.md --- docs/01-prerequisites.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/01-prerequisites.md b/docs/01-prerequisites.md index e09e680..320df32 100644 --- a/docs/01-prerequisites.md +++ b/docs/01-prerequisites.md @@ -4,11 +4,11 @@ In this tutorial, we use the [Google Cloud Platform](https://cloud.google.com/) to provision the compute infrastructure. You can [sign up](https://cloud.google.com/free/) for $300 in free credits, which will be more than sufficient to complete all of the labs in this tutorial. -NOTE: SEIS664 -- You do not need to install the Google Cloud SDK if you are using the Google Cloud Shell as recommended for SEIS664. It is already installed. +IMPORTANT: SEIS664 -- You do not need to install the Google Cloud SDK if you are using the Google Cloud Shell as recommended for SEIS664. It is already installed. ## Google Cloud Platform SDK -### Install the Google Cloud SDK +### (ONLY IF YOU ARE RUNNING LOCALLY) Install the Google Cloud SDK Follow the Google Cloud SDK [documentation](https://cloud.google.com/sdk/) to install and configure the `gcloud` command line utility for your platform. From e903a218e404faa3e297ef41752f4b003fabccd5 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Fri, 27 Mar 2020 19:50:00 -0500 Subject: [PATCH 08/93] Update 01-prerequisites.md --- docs/01-prerequisites.md | 31 ++++--------------------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/docs/01-prerequisites.md b/docs/01-prerequisites.md index 320df32..4a50582 100644 --- a/docs/01-prerequisites.md +++ b/docs/01-prerequisites.md @@ -2,32 +2,9 @@ ## Google Cloud Platform -In this tutorial, we use the [Google Cloud Platform](https://cloud.google.com/) to provision the compute infrastructure. You can [sign up](https://cloud.google.com/free/) for $300 in free credits, which will be more than sufficient to complete all of the labs in this tutorial. - -IMPORTANT: SEIS664 -- You do not need to install the Google Cloud SDK if you are using the Google Cloud Shell as recommended for SEIS664. It is already installed. - -## Google Cloud Platform SDK - -### (ONLY IF YOU ARE RUNNING LOCALLY) Install the Google Cloud SDK - -Follow the Google Cloud SDK [documentation](https://cloud.google.com/sdk/) to install and configure the `gcloud` command line utility for your platform. - -Verify the Google Cloud SDK version is 183.0.0 or higher: - -```bash -$ gcloud version -``` - -### Set Application Default Credentials - -This tutorial assumes Application Default Credentials (ADC) were set to authenticate to Google Cloud Platform API. - -Use the following gcloud command to acquire new user credentials to use for ADC. - -```bash -$ gcloud auth application-default login -``` +In this tutorial, we use the [Google Cloud Platform](https://cloud.google.com/) to provision the compute infrastructure. You have already signed up. +## Google Cloud Platform ### Set a Default Project, Compute Region and Zone This tutorial assumes a default compute region and zone have been configured. @@ -35,13 +12,13 @@ This tutorial assumes a default compute region and zone have been configured. Set a default compute region appropriate to your location ([GCP regions and zones](https://cloud.google.com/compute/docs/regions-zones)): ```bash -$ gcloud config set compute/region europe-west1 +$ gcloud config set compute/region us-central1 ``` Set a default compute zone appropriate to the zone: ```bash -$ gcloud config set compute/zone europe-west1-b +$ gcloud config set compute/zone us-central1-c ``` Verify the configuration settings: From c2c5380d37c3c03b689f64140ae648c99fb8e940 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Fri, 27 Mar 2020 20:31:07 -0500 Subject: [PATCH 09/93] Update 04-packer.md --- docs/04-packer.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/04-packer.md b/docs/04-packer.md index 6e54a67..e5eec54 100644 --- a/docs/04-packer.md +++ b/docs/04-packer.md @@ -14,7 +14,9 @@ Luckily, we can create custom machine images with required configuration and sof ## Install Packer -[Download](https://www.packer.io/downloads.html) and install Packer onto your system. +[Download](https://www.packer.io/downloads.html) and install Packer onto your system (this means the Google Cloud Shell). + +If you have issues, consult [this gist](https://gist.github.com/CharlesTBetz/20daf92169689e572873439439e1ad4f). Check the version to verify that it was installed: From 5adb1a5333259775bdec85664981886d38cf6d8a Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Fri, 27 Mar 2020 20:38:18 -0500 Subject: [PATCH 10/93] Update 01-prerequisites.md --- docs/01-prerequisites.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/01-prerequisites.md b/docs/01-prerequisites.md index 4a50582..ca6c353 100644 --- a/docs/01-prerequisites.md +++ b/docs/01-prerequisites.md @@ -4,6 +4,8 @@ In this tutorial, we use the [Google Cloud Platform](https://cloud.google.com/) to provision the compute infrastructure. You have already signed up. +Start in the Google Cloud Shell. + ## Google Cloud Platform ### Set a Default Project, Compute Region and Zone From dbfca3e22c1044cd30faa178fb7cccec5dc058d1 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Fri, 27 Mar 2020 20:39:42 -0500 Subject: [PATCH 11/93] Update 01-prerequisites.md --- docs/01-prerequisites.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/01-prerequisites.md b/docs/01-prerequisites.md index ca6c353..8f44e88 100644 --- a/docs/01-prerequisites.md +++ b/docs/01-prerequisites.md @@ -4,7 +4,7 @@ In this tutorial, we use the [Google Cloud Platform](https://cloud.google.com/) to provision the compute infrastructure. You have already signed up. -Start in the Google Cloud Shell. +Start in the Google Cloud Shell. [(review)](https://cloud.google.com/shell/docs/features) ## Google Cloud Platform ### Set a Default Project, Compute Region and Zone From d9b5cdb2f3ce5a779d6e9662c68746d537c25c2a Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Fri, 27 Mar 2020 20:42:08 -0500 Subject: [PATCH 12/93] Update 01-prerequisites.md --- docs/01-prerequisites.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/01-prerequisites.md b/docs/01-prerequisites.md index 8f44e88..f5dd98e 100644 --- a/docs/01-prerequisites.md +++ b/docs/01-prerequisites.md @@ -4,7 +4,7 @@ In this tutorial, we use the [Google Cloud Platform](https://cloud.google.com/) to provision the compute infrastructure. You have already signed up. -Start in the Google Cloud Shell. [(review)](https://cloud.google.com/shell/docs/features) +Start in the Google Cloud Shell. [(review)](https://cloud.google.com/shell/docs/using-cloud-shell) ## Google Cloud Platform ### Set a Default Project, Compute Region and Zone From b1a6dc67b8a76b042cf5130ecf467fd6a4c291c7 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Fri, 27 Mar 2020 21:02:42 -0500 Subject: [PATCH 13/93] Update 02-manual-operations.md --- docs/02-manual-operations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/02-manual-operations.md b/docs/02-manual-operations.md index ebefdbb..615ae32 100644 --- a/docs/02-manual-operations.md +++ b/docs/02-manual-operations.md @@ -57,7 +57,7 @@ $ ssh-add -l To start the application, you need to first configure the environment for running it. -Connect to the started VM via SSH: +Connect to the started VM via SSH using the following two commands: ```bash $ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe raddit-instance-2) From c8078bc6bcaf3022028fe7e37dfb58f81e02d844 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sat, 28 Mar 2020 21:28:26 -0500 Subject: [PATCH 14/93] Update 05-terraform.md --- docs/05-terraform.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/05-terraform.md b/docs/05-terraform.md index 5cb121f..5fb767e 100644 --- a/docs/05-terraform.md +++ b/docs/05-terraform.md @@ -36,9 +36,11 @@ So we see here 2 clear problems: The second problem is dealt by source control tools like `git`, while the first one is solved by using tools like Terraform. Let's find out how. -## Install Terraform +## Terraform -[Download](https://www.terraform.io/downloads.html) and install Terraform on your system. +Terraform is already installed on Google Cloud Shell. + +If you want to install it on a laptop or VM, you can [download here](https://www.terraform.io/downloads.html). Make sure Terraform version is => 0.11.0: From 49d4390cccb72c4a49b04245d6d91c481f85e9af Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sat, 28 Mar 2020 21:36:38 -0500 Subject: [PATCH 15/93] Update 04-packer.md --- docs/04-packer.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/04-packer.md b/docs/04-packer.md index e5eec54..6552357 100644 --- a/docs/04-packer.md +++ b/docs/04-packer.md @@ -44,7 +44,7 @@ Create a `raddit-base-image.json` file inside the `packer` directory with the fo { "type": "googlecompute", "project_id": "infrastructure-as-code", - "zone": "europe-west1-b", + "zone": "us-central1-c", "machine_type": "g1-small", "source_image_family": "ubuntu-1604-lts", "image_name": "raddit-base-{{isotime `20060102-150405`}}", @@ -78,7 +78,7 @@ Your template should look similar to this one: { "type": "googlecompute", "project_id": "infrastructure-as-code", - "zone": "europe-west1-b", + "zone": "us-central1-c", "machine_type": "g1-small", "source_image_family": "ubuntu-1604-lts", "image_name": "raddit-base-{{isotime `20060102-150405`}}", From b6ad2acf562ed9876e60a0514b4eca9882a147ab Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sun, 29 Mar 2020 12:58:54 -0500 Subject: [PATCH 16/93] Update 04-packer.md --- docs/04-packer.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/04-packer.md b/docs/04-packer.md index 6552357..0cd940a 100644 --- a/docs/04-packer.md +++ b/docs/04-packer.md @@ -126,6 +126,8 @@ $ gcloud compute instances create raddit-instance-4 \ Copy `deploy.sh` script to the created VM: +_instructor's note: I am currently encountering an error here - the image does not seem to be correctly set up with my ssh public key._ + ```bash $ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe raddit-instance-4) $ scp ./scripts/deploy.sh raddit-user@${INSTANCE_IP}:/home/raddit-user From 1ebe5bc4888323ca5e70b050bacb6bf2f781fc45 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sun, 29 Mar 2020 14:50:01 -0500 Subject: [PATCH 17/93] Update 03-scripts.md --- docs/03-scripts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/03-scripts.md b/docs/03-scripts.md index 6d775e1..86c7357 100644 --- a/docs/03-scripts.md +++ b/docs/03-scripts.md @@ -111,7 +111,7 @@ $ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].nat $ scp -r ./scripts raddit-user@${INSTANCE_IP}:/home/raddit-user ``` NOTE: If you get an `offending ECDSA key` error, use the suggested removal command. - +NOTE: If you get the error `Permission denied (publickey).`, this probably means that your ssh-agent no longer has the raddit-user private key added. Check via issuing `ssh-add -l`. You should see something like `2048 SHA256:bII5VsQY3fCWXEai0lUeChEYPaagMXun3nB9U2eoUEM /home/betz4871/.ssh/raddit-user (RSA)`. If you do not, re-issue the command `ssh-add ~/.ssh/raddit-user` and re-confirm with `ssh-add -l`. Connect to the VM via SSH: ```bash $ ssh raddit-user@${INSTANCE_IP} From 0f175c6c072686ca8fd18dd733f4dfb2aac2c995 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sun, 29 Mar 2020 14:50:31 -0500 Subject: [PATCH 18/93] Update 03-scripts.md --- docs/03-scripts.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/03-scripts.md b/docs/03-scripts.md index 86c7357..9da8769 100644 --- a/docs/03-scripts.md +++ b/docs/03-scripts.md @@ -111,6 +111,7 @@ $ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].nat $ scp -r ./scripts raddit-user@${INSTANCE_IP}:/home/raddit-user ``` NOTE: If you get an `offending ECDSA key` error, use the suggested removal command. + NOTE: If you get the error `Permission denied (publickey).`, this probably means that your ssh-agent no longer has the raddit-user private key added. Check via issuing `ssh-add -l`. You should see something like `2048 SHA256:bII5VsQY3fCWXEai0lUeChEYPaagMXun3nB9U2eoUEM /home/betz4871/.ssh/raddit-user (RSA)`. If you do not, re-issue the command `ssh-add ~/.ssh/raddit-user` and re-confirm with `ssh-add -l`. Connect to the VM via SSH: ```bash From ee3af8d51f5363d175f3d7df5011276b9dc2c4d4 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sun, 29 Mar 2020 14:51:10 -0500 Subject: [PATCH 19/93] Update 03-scripts.md --- docs/03-scripts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/03-scripts.md b/docs/03-scripts.md index 9da8769..30b0606 100644 --- a/docs/03-scripts.md +++ b/docs/03-scripts.md @@ -112,7 +112,7 @@ $ scp -r ./scripts raddit-user@${INSTANCE_IP}:/home/raddit-user ``` NOTE: If you get an `offending ECDSA key` error, use the suggested removal command. -NOTE: If you get the error `Permission denied (publickey).`, this probably means that your ssh-agent no longer has the raddit-user private key added. Check via issuing `ssh-add -l`. You should see something like `2048 SHA256:bII5VsQY3fCWXEai0lUeChEYPaagMXun3nB9U2eoUEM /home/betz4871/.ssh/raddit-user (RSA)`. If you do not, re-issue the command `ssh-add ~/.ssh/raddit-user` and re-confirm with `ssh-add -l`. +NOTE: If you get the error `Permission denied (publickey).`, this probably means that your ssh-agent no longer has the raddit-user private key added. This easily happens if the Google Cloud Shell goes to sleep and wipes out your session. Check via issuing `ssh-add -l`. You should see something like `2048 SHA256:bII5VsQY3fCWXEai0lUeChEYPaagMXun3nB9U2eoUEM /home/betz4871/.ssh/raddit-user (RSA)`. If you do not, re-issue the command `ssh-add ~/.ssh/raddit-user` and re-confirm with `ssh-add -l`. Connect to the VM via SSH: ```bash $ ssh raddit-user@${INSTANCE_IP} From 01fd6afd4326cfef63222bc1dcd337411c2b74ec Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sun, 29 Mar 2020 15:06:51 -0500 Subject: [PATCH 20/93] Update 04-packer.md --- docs/04-packer.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/04-packer.md b/docs/04-packer.md index 0cd940a..74a9a59 100644 --- a/docs/04-packer.md +++ b/docs/04-packer.md @@ -111,6 +111,8 @@ Build the image for your application: $ packer build ./packer/raddit-base-image.json ``` +If you go to the [Compute Engine Images](https://console.cloud.google.com/compute/images) page you should see your new custom image. + ## Launch a VM with your custom built machine image Once the image is built, use it as a boot disk to start a VM: From 0003eaca5e072064e8e7728737722af195259218 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sun, 29 Mar 2020 15:10:53 -0500 Subject: [PATCH 21/93] Update 04-packer.md --- docs/04-packer.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/04-packer.md b/docs/04-packer.md index 74a9a59..2aa4b49 100644 --- a/docs/04-packer.md +++ b/docs/04-packer.md @@ -135,6 +135,10 @@ $ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].nat $ scp ./scripts/deploy.sh raddit-user@${INSTANCE_IP}:/home/raddit-user ``` +NOTE: If you get an offending ECDSA key error, use the suggested removal command. + +NOTE: If you get the error Permission denied (publickey)., this probably means that your ssh-agent no longer has the raddit-user private key added. This easily happens if the Google Cloud Shell goes to sleep and wipes out your session. Check via issuing ssh-add -l. You should see something like 2048 SHA256:bII5VsQY3fCWXEai0lUeChEYPaagMXun3nB9U2eoUEM /home/betz4871/.ssh/raddit-user (RSA). If you do not, re-issue the command ssh-add ~/.ssh/raddit-user and re-confirm with ssh-add -l. Connect to the VM via SSH: + Connect to the VM via SSH: ```bash From 9bba4fa181d500d4bfce3e27d13ab813a62bcfbd Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sun, 29 Mar 2020 15:15:45 -0500 Subject: [PATCH 22/93] Update 04-packer.md --- docs/04-packer.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/04-packer.md b/docs/04-packer.md index 2aa4b49..1b2bd61 100644 --- a/docs/04-packer.md +++ b/docs/04-packer.md @@ -128,8 +128,6 @@ $ gcloud compute instances create raddit-instance-4 \ Copy `deploy.sh` script to the created VM: -_instructor's note: I am currently encountering an error here - the image does not seem to be correctly set up with my ssh public key._ - ```bash $ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe raddit-instance-4) $ scp ./scripts/deploy.sh raddit-user@${INSTANCE_IP}:/home/raddit-user From d0b961709ef2f63c98ea36b513e44dbb906c57f0 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sun, 29 Mar 2020 15:16:54 -0500 Subject: [PATCH 23/93] Update 04-packer.md --- docs/04-packer.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/04-packer.md b/docs/04-packer.md index 1b2bd61..83f045f 100644 --- a/docs/04-packer.md +++ b/docs/04-packer.md @@ -135,7 +135,7 @@ $ scp ./scripts/deploy.sh raddit-user@${INSTANCE_IP}:/home/raddit-user NOTE: If you get an offending ECDSA key error, use the suggested removal command. -NOTE: If you get the error Permission denied (publickey)., this probably means that your ssh-agent no longer has the raddit-user private key added. This easily happens if the Google Cloud Shell goes to sleep and wipes out your session. Check via issuing ssh-add -l. You should see something like 2048 SHA256:bII5VsQY3fCWXEai0lUeChEYPaagMXun3nB9U2eoUEM /home/betz4871/.ssh/raddit-user (RSA). If you do not, re-issue the command ssh-add ~/.ssh/raddit-user and re-confirm with ssh-add -l. Connect to the VM via SSH: +NOTE: If you get the error `Permission denied (publickey).`, this probably means that your ssh-agent no longer has the raddit-user private key added. This easily happens if the Google Cloud Shell goes to sleep and wipes out your session. Check via issuing `ssh-add -l`. You should see something like `2048 SHA256:bII5VsQY3fCWXEai0lUeChEYPaagMXun3nB9U2eoUEM /home/betz4871/.ssh/raddit-user (RSA)`. If you do not, re-issue the command `ssh-add ~/.ssh/raddit-user` and re-confirm with `ssh-add -l`. Connect to the VM via SSH: From eccb9146bc3b0b70e14aeac78c25e58ef5bf8bbb Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sun, 29 Mar 2020 15:19:03 -0500 Subject: [PATCH 24/93] Update 05-terraform.md --- docs/05-terraform.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/05-terraform.md b/docs/05-terraform.md index 5fb767e..b709a5e 100644 --- a/docs/05-terraform.md +++ b/docs/05-terraform.md @@ -56,7 +56,7 @@ Create a new directory called `terraform` inside your `iac-tutorial` repo, which _Terraform allows you to describe the desired state of your infrastructure and makes sure your desired state meets the actual state._ -Terraform uses **resources** to describe different infrastructure components. If you want to use Terraform to manage some infrastructure component, you should first make sure there is a resource for that component for that particular platform. +Terraform uses [**resources**](https://www.terraform.io/docs/configuration/resources.html) to describe different infrastructure components. If you want to use Terraform to manage some infrastructure component, you should first make sure there is a resource for that component for that particular platform. Let's use Terraform syntax to describe a VM instance that we want to be running. From d6f4c6437939d9d2a4ace8fbacc3dfa8faa8c443 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sun, 29 Mar 2020 15:22:14 -0500 Subject: [PATCH 25/93] Update 05-terraform.md --- docs/05-terraform.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/05-terraform.md b/docs/05-terraform.md index b709a5e..81bccb5 100644 --- a/docs/05-terraform.md +++ b/docs/05-terraform.md @@ -66,7 +66,7 @@ Create a Terraform configuration file called `main.tf` inside the `terraform` di resource "google_compute_instance" "raddit" { name = "raddit-instance" machine_type = "n1-standard-1" - zone = "europe-west1-b" + zone = "us-central1-c" # boot disk specifications boot_disk { From 7d1a247b3aabd3fe184fc55bfc4237808bbd8797 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sun, 29 Mar 2020 15:23:23 -0500 Subject: [PATCH 26/93] Update 05-terraform.md --- docs/05-terraform.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/05-terraform.md b/docs/05-terraform.md index 81bccb5..cadcea6 100644 --- a/docs/05-terraform.md +++ b/docs/05-terraform.md @@ -99,7 +99,7 @@ Create another file inside `terraform` folder and call it `providers.tf`. Put pr provider "google" { version = "~> 1.4.0" project = "infrastructure-as-code" - region = "europe-west1" + region = "us-central1-c" } ``` From 4a9b5008f0774489581013dea0a44dc09a5dfd17 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sun, 29 Mar 2020 15:25:02 -0500 Subject: [PATCH 27/93] Update 05-terraform.md --- docs/05-terraform.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/05-terraform.md b/docs/05-terraform.md index cadcea6..cb153ad 100644 --- a/docs/05-terraform.md +++ b/docs/05-terraform.md @@ -97,7 +97,7 @@ Create another file inside `terraform` folder and call it `providers.tf`. Put pr ``` provider "google" { - version = "~> 1.4.0" + version = "~> 2.5.0" project = "infrastructure-as-code" region = "us-central1-c" } From 45454ae36c1d398f6292cc044238000ee99dc8bf Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sun, 29 Mar 2020 15:40:50 -0500 Subject: [PATCH 28/93] Update 05-terraform.md --- docs/05-terraform.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/05-terraform.md b/docs/05-terraform.md index cb153ad..9bddf2a 100644 --- a/docs/05-terraform.md +++ b/docs/05-terraform.md @@ -183,6 +183,8 @@ Make sure that your application became inaccessible via port 9292 and SSH connec Then add appropriate resources into `main.tf` file. Your final version of `main.tf` file should look similar to this (change the ssh key file path, if necessary): +_Instructor's note 3/29/2020: there is some issue with this Terraform file, resulting in errors of missing required argument and unsupported block type. Currently troubleshooting._ + ``` resource "google_compute_instance" "raddit" { name = "raddit-instance" From c73c0169d1920afb44148bd30d762a6ae3af76bb Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sun, 29 Mar 2020 15:49:32 -0500 Subject: [PATCH 29/93] Update 05-terraform.md --- docs/05-terraform.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/05-terraform.md b/docs/05-terraform.md index 9bddf2a..0047d30 100644 --- a/docs/05-terraform.md +++ b/docs/05-terraform.md @@ -206,7 +206,7 @@ resource "google_compute_instance" "raddit" { } resource "google_compute_project_metadata" "raddit" { - metadata { + metadata = { ssh-keys = "raddit-user:${file("~/.ssh/raddit-user.pub")}" // path to ssh key file } } From d22b0645d2d6e3667e9b71d6864cecbe065d4bab Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sun, 29 Mar 2020 15:49:56 -0500 Subject: [PATCH 30/93] Update 05-terraform.md --- docs/05-terraform.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/05-terraform.md b/docs/05-terraform.md index 0047d30..c2dd769 100644 --- a/docs/05-terraform.md +++ b/docs/05-terraform.md @@ -183,7 +183,6 @@ Make sure that your application became inaccessible via port 9292 and SSH connec Then add appropriate resources into `main.tf` file. Your final version of `main.tf` file should look similar to this (change the ssh key file path, if necessary): -_Instructor's note 3/29/2020: there is some issue with this Terraform file, resulting in errors of missing required argument and unsupported block type. Currently troubleshooting._ ``` resource "google_compute_instance" "raddit" { From 51d938d02f48846dde6b28dc6325911f1c3e8ee8 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sun, 29 Mar 2020 15:56:39 -0500 Subject: [PATCH 31/93] Update 05-terraform.md --- docs/05-terraform.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/05-terraform.md b/docs/05-terraform.md index c2dd769..4a51a26 100644 --- a/docs/05-terraform.md +++ b/docs/05-terraform.md @@ -188,7 +188,7 @@ Then add appropriate resources into `main.tf` file. Your final version of `main. resource "google_compute_instance" "raddit" { name = "raddit-instance" machine_type = "n1-standard-1" - zone = "europe-west1-b" + zone = "us-central1-c" # boot disk specifications boot_disk { From f9889815d9ef4fd87cf197abb6e3bd80af12d91e Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sun, 29 Mar 2020 18:27:51 -0500 Subject: [PATCH 32/93] Update 05-terraform.md --- docs/05-terraform.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/05-terraform.md b/docs/05-terraform.md index 4a51a26..199f400 100644 --- a/docs/05-terraform.md +++ b/docs/05-terraform.md @@ -143,6 +143,10 @@ $ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].nat $ scp ../scripts/deploy.sh raddit-user@${INSTANCE_IP}:/home/raddit-user ``` +NOTE: If you get an offending ECDSA key error, use the suggested removal command. + +NOTE: If you get the error `Permission denied (publickey).`, this probably means that your ssh-agent no longer has the raddit-user private key added. This easily happens if the Google Cloud Shell goes to sleep and wipes out your session. Check via issuing `ssh-add -l`. You should see something like `2048 SHA256:bII5VsQY3fCWXEai0lUeChEYPaagMXun3nB9U2eoUEM /home/betz4871/.ssh/raddit-user (RSA)`. If you do not, re-issue the command `ssh-add ~/.ssh/raddit-user` and re-confirm with `ssh-add -l`. + Connect to the VM via SSH: ```bash From a2a7b0773bd423fbe511df964bf981248fa24c99 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sun, 29 Mar 2020 19:32:29 -0500 Subject: [PATCH 33/93] Update 05-terraform.md --- docs/05-terraform.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/05-terraform.md b/docs/05-terraform.md index 199f400..a85ed92 100644 --- a/docs/05-terraform.md +++ b/docs/05-terraform.md @@ -247,7 +247,7 @@ Create another configuration file inside `terraform` directory and call it `outp ``` output "raddit_public_ip" { - value = "${google_compute_instance.raddit.network_interface.0.access_config.0.assigned_nat_ip}" + value = "${google_compute_instance.raddit.network_interface.0.access_config.0.nat_ip}" } ``` From a47bf8bdf5751ebd960c8edd362b5a869a9416cc Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sun, 29 Mar 2020 19:58:50 -0500 Subject: [PATCH 34/93] Update 06-ansible.md --- docs/06-ansible.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/06-ansible.md b/docs/06-ansible.md index 726bf2d..5bce19f 100644 --- a/docs/06-ansible.md +++ b/docs/06-ansible.md @@ -22,9 +22,16 @@ This is exactly what CM tools do. So let's check it out using Ansible as an exam NOTE: this lab assumes Ansible v2.4 is installed. It may not work as expected with other versions as things change quickly. -You can follow the instructions on how to install Ansible on your system from [official documentation](http://docs.ansible.com/ansible/latest/intro_installation.html). +Issue the following commands: -I personally prefer installing it via [pip](http://docs.ansible.com/ansible/latest/intro_installation.html#latest-releases-via-pip) on my Linux machine. +``` +$ sudo apt update +$ sudo apt install software-properties-common +$ sudo apt-add-repository --yes --update ppa:ansible/ansible +$ sudo apt install ansible +``` + +If you have issues, reference the instructions on how to install Ansible on your system from [official documentation](http://docs.ansible.com/ansible/latest/intro_installation.html). Verify that Ansible was installed by checking the version: From 4c9d16fad7931001e2fd8100e9e796cab9f6dbf5 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sun, 29 Mar 2020 21:01:52 -0500 Subject: [PATCH 35/93] Update 07-vagrant.md --- docs/07-vagrant.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/07-vagrant.md b/docs/07-vagrant.md index 70a6d5d..ea3243b 100644 --- a/docs/07-vagrant.md +++ b/docs/07-vagrant.md @@ -1,7 +1,9 @@ -## Vagrant +## Vagrant (OPTIONAL, for local laptops/workstations only) In this lab, we're going to learn about [Vagrant](https://www.vagrantup.com/) which is another tool that implements IaC approach and is often used for creating development environments. +_This lab is optional for those of you want to learn Vagrant on your personal laptops and workstations. It will not work on GCE virtual machines, university workstations, or Google Cloud Shell. The alternative to local development is to create a local development VM in the cloud. You may skip to [Docker](08-docker.md)_. + ## Intro Before this lab, our main focus was on how to create and manage an environment where our application runs and is accessible to the public. Let's call that environment `production` for the sake of simplicity of referring to that later. From 0b496f8d9324d8b23e37623fb8c564ffbc7de67a Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sun, 29 Mar 2020 21:08:58 -0500 Subject: [PATCH 36/93] Update 08-docker.md --- docs/08-docker.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/08-docker.md b/docs/08-docker.md index 2e161d7..cedaa6c 100644 --- a/docs/08-docker.md +++ b/docs/08-docker.md @@ -16,7 +16,9 @@ They have some significant advantages over VMs in terms of implementing Immutabl Let's try to implement `Immutable Infrastructure` model with Docker containers, while paying special attention to the `Dockerfile` part as a way to practice `Infrastructure as Code` approach. -## Install Docker Engine +## (FOR PERSONAL LAPTOPS AND WORKSTATIONS ONLY) Install Docker Engine + +_Docker is already installed on Google Cloud Shell._ The [Docker Engine](https://docs.docker.com/engine/docker-overview/#docker-engine) is the daemon that gets installed on the system and allows you to manage containers with simple CLI. From d2d4d9fd1776e28be709abbc75cc58df6500fc94 Mon Sep 17 00:00:00 2001 From: Charles Betz Date: Mon, 30 Mar 2020 18:37:19 -0500 Subject: [PATCH 37/93] image --- .DS_Store | Bin 0 -> 6148 bytes webPreview.png | Bin 0 -> 200952 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 .DS_Store create mode 100644 webPreview.png diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..2e97c10d5cd61d82bb3267d87d58d86009649a28 GIT binary patch literal 6148 zcmeHKu};G<5WPz?1hI64&0m1jKL}M=kPt&xhAN5FR!Zbbg@q~q!NMo-4g3`w@9a|> ztA!CExRcJ$x%1hV=g7_x5zn968PSx83aDUqhUSEbU-U>awMZPx9NlWRXnemt8fk{# z%7E-$PFJ*|4Lv6Nm#uHdzTQ_=-T4}}V)Oj5e7JnSPW|n-{_T47ZA@MfigjD^)X=Wo zHC^p}hmYuBOp@Wdm$7f0%zZmbMknn3A(>PDBpH5_WK4_!W55{rM-0eu3n|VcT5Aj# z1IECf0XZKWRIpSWB8H;_O(+3?Im|)Oms&z%oMNdsM1%*zF%%d>SxXF#;jlX$S1Jw> zV>q!EAFM00Rwx`+$NVk~CoUseYYZ3zDFc)EIF;b3~*j8sySXs z@7AN2le^YImrxNIH$>crpcAfQjFqeS0vZH%Cmmp^I7Ea6Vm|_b25XFgA7$VZm6%B8 literal 0 HcmV?d00001 diff --git a/webPreview.png b/webPreview.png new file mode 100644 index 0000000000000000000000000000000000000000..2842684aef99325423a0f8666f6c0777ad5087a4 GIT binary patch literal 200952 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 z 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 z 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 literal 0 HcmV?d00001 From e7c3f8850045fbba9124e7bd7b92c34cf1945370 Mon Sep 17 00:00:00 2001 From: Charles Betz Date: Mon, 30 Mar 2020 18:40:51 -0500 Subject: [PATCH 38/93] image --- webPreview.png => img/webPreview.png | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename webPreview.png => img/webPreview.png (100%) diff --git a/webPreview.png b/img/webPreview.png similarity index 100% rename from webPreview.png rename to img/webPreview.png From f873201918521f045cf1807a8ac990d14e3f7ee3 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Mon, 30 Mar 2020 18:53:23 -0500 Subject: [PATCH 39/93] Update 08-docker.md --- docs/08-docker.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/08-docker.md b/docs/08-docker.md index cedaa6c..ae02e2e 100644 --- a/docs/08-docker.md +++ b/docs/08-docker.md @@ -183,7 +183,11 @@ Port mapping option (`--publish`) that we passed to the command is used to make ## Access Application -The application should be accessible to your at http://localhost:9292 +To access via the Google Cloud Shell, use the Web Preview: + +![](../img/webPreview.png) + +(If you are running this tutorial locally, the application should be accessible to your at http://localhost:9292) ## Save and commit the work From db5dd6881bd59fc02231ddf5e35eb8a03193dfd6 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Mon, 30 Mar 2020 19:03:55 -0500 Subject: [PATCH 40/93] Update 08-docker.md --- docs/08-docker.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/08-docker.md b/docs/08-docker.md index ae02e2e..6d90382 100644 --- a/docs/08-docker.md +++ b/docs/08-docker.md @@ -187,6 +187,22 @@ To access via the Google Cloud Shell, use the Web Preview: ![](../img/webPreview.png) +AT THIS WRITING, you cannot select port 9292. Select any offered port: + +![](../img/webPreviewPorts.png) + +You will receive an error. Notice your URL which should look something like this: + +`https://8080-dot-8658285-dot-devshell.appspot.com/?authuser=0` + +The first four digits after the `https://` represent the desired port. Change this to 9292, for example: + +`https://9292-dot-8658285-dot-devshell.appspot.com/?authuser=0` + +(Do not try to use that URL. Modify the one provided you.) + +You should now be able to see the application. + (If you are running this tutorial locally, the application should be accessible to your at http://localhost:9292) ## Save and commit the work From ab93ecc3280d6336a629684b0847e458a7b2956c Mon Sep 17 00:00:00 2001 From: Charles Betz Date: Mon, 30 Mar 2020 19:04:48 -0500 Subject: [PATCH 41/93] images --- img/webPreviewPort.png | Bin 0 -> 65056 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 img/webPreviewPort.png diff --git a/img/webPreviewPort.png b/img/webPreviewPort.png new file mode 100644 index 0000000000000000000000000000000000000000..87570cd6dfe16c3811c185be41bbc12c40c377fe GIT binary patch literal 65056 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 literal 0 HcmV?d00001 From ac3c24c9fb88ce5aa3f746463bb8b2a64a6a65c4 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Mon, 30 Mar 2020 19:05:59 -0500 Subject: [PATCH 42/93] Update 08-docker.md --- docs/08-docker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/08-docker.md b/docs/08-docker.md index 6d90382..9e1d3d0 100644 --- a/docs/08-docker.md +++ b/docs/08-docker.md @@ -189,7 +189,7 @@ To access via the Google Cloud Shell, use the Web Preview: AT THIS WRITING, you cannot select port 9292. Select any offered port: -![](../img/webPreviewPorts.png) +![](../img/webPreviewPort.png) You will receive an error. Notice your URL which should look something like this: From cefe752974ec0120e9bf6cf4a86dc59aa813d7f4 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Mon, 30 Mar 2020 19:08:15 -0500 Subject: [PATCH 43/93] Update 08-docker.md --- docs/08-docker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/08-docker.md b/docs/08-docker.md index 9e1d3d0..cf4a829 100644 --- a/docs/08-docker.md +++ b/docs/08-docker.md @@ -189,7 +189,7 @@ To access via the Google Cloud Shell, use the Web Preview: AT THIS WRITING, you cannot select port 9292. Select any offered port: -![](../img/webPreviewPort.png) +![](../img/webPreviewPort.png =250x) You will receive an error. Notice your URL which should look something like this: From ee91124a708e5f976d83c243ee8bab4de2d23796 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Mon, 30 Mar 2020 19:10:37 -0500 Subject: [PATCH 44/93] Update 08-docker.md --- docs/08-docker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/08-docker.md b/docs/08-docker.md index cf4a829..9e1d3d0 100644 --- a/docs/08-docker.md +++ b/docs/08-docker.md @@ -189,7 +189,7 @@ To access via the Google Cloud Shell, use the Web Preview: AT THIS WRITING, you cannot select port 9292. Select any offered port: -![](../img/webPreviewPort.png =250x) +![](../img/webPreviewPort.png) You will receive an error. Notice your URL which should look something like this: From 9384f10602a9253017161951af1f5f436afca4a3 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Mon, 30 Mar 2020 19:17:12 -0500 Subject: [PATCH 45/93] Update 09-docker-compose.md --- docs/09-docker-compose.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/09-docker-compose.md b/docs/09-docker-compose.md index 4775d1f..288612d 100644 --- a/docs/09-docker-compose.md +++ b/docs/09-docker-compose.md @@ -14,7 +14,7 @@ To make the management of our local container infrastructure easier and more rel **Docker Compose** is exactly the tool we need. Let's see how we can use it. -## Install Docker Compose +## (FOR PERSONAL LAPTOPS AND WORKSTATIONS ONLY) Install Docker Compose Follow the official documentation on [how to install Docker Compose](https://docs.docker.com/compose/install/) on your system. From 60b1fdce35b5625f05df484e78c4853443ddd9e3 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Tue, 31 Mar 2020 18:10:55 -0500 Subject: [PATCH 46/93] Update 10-kubernetes.md --- docs/10-kubernetes.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/10-kubernetes.md b/docs/10-kubernetes.md index 3393cb2..04f8ccd 100644 --- a/docs/10-kubernetes.md +++ b/docs/10-kubernetes.md @@ -20,7 +20,9 @@ Kubernetes is the most widely used orchestration platform for running and managi Let's try to run our `raddit` application on a Kubernetes cluster. -## Install Kubectl +## (FOR PERSONAL LAPTOPS AND WORKSTATIONS ONLY) Install Kubectl + +Kubectl is installed on the Google Cloud Shell. [Kubectl](https://kubernetes.io/docs/reference/kubectl/overview/) is command line tool that we will use to run commands against the Kubernetes cluster. From 7f6f3e8e8770a84153fe264bb5589b7029ea7414 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Fri, 3 Apr 2020 17:16:22 -0500 Subject: [PATCH 47/93] Update 08-docker.md --- docs/08-docker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/08-docker.md b/docs/08-docker.md index 9e1d3d0..d47fb7e 100644 --- a/docs/08-docker.md +++ b/docs/08-docker.md @@ -45,7 +45,7 @@ Dockerfile contains `instructions` on how the image should be built. Here are so Let's use these instructions to create a Docker container image for our raddit application. -Create a file called `Dockerfile` inside your `iac-tutorial` repo with the following content: +Create a folder called `docker` inside your `iac-tutorial` repo. Inside of that new directory, create a text file called `Dockerfile` with the following content: ``` # Use base image with Ruby installed From b152cd6d2e635660c5b236296f8172c985b38e69 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Fri, 3 Apr 2020 17:18:04 -0500 Subject: [PATCH 48/93] Update 08-docker.md --- docs/08-docker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/08-docker.md b/docs/08-docker.md index d47fb7e..6593c11 100644 --- a/docs/08-docker.md +++ b/docs/08-docker.md @@ -104,7 +104,7 @@ CMD ["puma"] ## Build Container Image -Once you defined how your image should be built, run the following command inside `iac-tutorial` directory to create a container image for raddit application: +Once you defined how your image should be built, run the following command inside `docker` directory to create a container image for raddit application: ```bash $ docker build --tag raddit . From 218b65df59574e092973929ffca62d4353913adf Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Fri, 3 Apr 2020 18:36:46 -0500 Subject: [PATCH 49/93] Update 10-kubernetes.md --- docs/10-kubernetes.md | 50 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/docs/10-kubernetes.md b/docs/10-kubernetes.md index 04f8ccd..f1c4741 100644 --- a/docs/10-kubernetes.md +++ b/docs/10-kubernetes.md @@ -48,12 +48,54 @@ We'll use [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/ We'll describe a Kubernetes cluster using Terraform so that we can manage it through code. -Create a directory named `terraform` inside `kubernetes` directory. Download a bundle of Terraform configuration files into the created `terraform` directory. +Create a directory named `terraform` inside `kubernetes` directory. Create three files within it: ```bash -$ wget https://github.com/Artemmkin/gke-terraform/raw/master/gke-terraform.zip -$ unzip gke-terraform.zip -d kubernetes/terraform -$ rm gke-terraform.zip +variables.tf +terraform.tfvars +main.tf +``` + +### variables.tf +```bash +# Provider configuration variables +variable "project_id" { + description = "Project ID in GCP" +} + +variable "region" { + description = "Region in which to manage GCP resources" +} + +# Cluster configuration variables +variable "cluster_name" { + description = "The name of the cluster, unique within the project and zone" +} + +variable "zone" { + description = "The zone in which nodes specified in initial_node_count should be created in" +} +© 2020 GitHub, Inc. +Terms +Privacy + + +``` +### terraform.tfvars +```bash +// define provider configuration variables +project_id = "proven-sum-252123" # project in which to create a cluster +region = "us-central1" # region in which to create a cluster + +// define Kubernetes cluster variables +cluster_name = "iac-tutorial-cluster" # cluster name +zone = "us-central1-c" # zone in which to create a cluster nodes + +``` + +### variables.tf +```bash + ``` We'll use this Terraform code to create a Kubernetes cluster. From 35ab8d6d49444e326664753dd4c2ecdcab38f93a Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Fri, 3 Apr 2020 18:37:27 -0500 Subject: [PATCH 50/93] Update 10-kubernetes.md --- docs/10-kubernetes.md | 53 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/docs/10-kubernetes.md b/docs/10-kubernetes.md index f1c4741..00571e1 100644 --- a/docs/10-kubernetes.md +++ b/docs/10-kubernetes.md @@ -93,8 +93,59 @@ zone = "us-central1-c" # zone in which to create a cluster ``` -### variables.tf +### main.tf ```bash +resource "google_container_cluster" "primary" { + name = "${var.cluster_name}" + location = "${var.zone}" + initial_node_count = 3 + + master_auth { + username = "" + password = "" + + client_certificate_config { + issue_client_certificate = false + } + } + + # configure kubectl to talk to the cluster + provisioner "local-exec" { + command = "gcloud container clusters get-credentials ${var.cluster_name} --zone ${var.zone} --project ${var.project_id}" + } + + node_config { + oauth_scopes = [ + "https://www.googleapis.com/auth/compute", + "https://www.googleapis.com/auth/devstorage.read_only", + "https://www.googleapis.com/auth/logging.write", + "https://www.googleapis.com/auth/monitoring", + ] + + metadata = { + disable-legacy-endpoints = "true" + } + + tags = ["iac-kubernetes"] + } + + timeouts { + create = "30m" + update = "40m" + } +} + +# create firewall rule to allow access to application +resource "google_compute_firewall" "nodeports" { + name = "node-port-range" + network = "default" + + allow { + protocol = "tcp" + ports = ["30000-32767"] + } + source_ranges = ["0.0.0.0/0"] +} ``` From ef62fe7a4aa849419c402d38e4085e997de6032a Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Fri, 3 Apr 2020 19:43:04 -0500 Subject: [PATCH 51/93] Update 03-scripts.md --- docs/03-scripts.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/03-scripts.md b/docs/03-scripts.md index 30b0606..d99404b 100644 --- a/docs/03-scripts.md +++ b/docs/03-scripts.md @@ -112,7 +112,12 @@ $ scp -r ./scripts raddit-user@${INSTANCE_IP}:/home/raddit-user ``` NOTE: If you get an `offending ECDSA key` error, use the suggested removal command. -NOTE: If you get the error `Permission denied (publickey).`, this probably means that your ssh-agent no longer has the raddit-user private key added. This easily happens if the Google Cloud Shell goes to sleep and wipes out your session. Check via issuing `ssh-add -l`. You should see something like `2048 SHA256:bII5VsQY3fCWXEai0lUeChEYPaagMXun3nB9U2eoUEM /home/betz4871/.ssh/raddit-user (RSA)`. If you do not, re-issue the command `ssh-add ~/.ssh/raddit-user` and re-confirm with `ssh-add -l`. +NOTE: If you get the error `Permission denied (publickey).`, this probably means that your ssh-agent no longer has the raddit-user private key added. This easily happens if the Google Cloud Shell goes to sleep and wipes out your session. Check via issuing `ssh-add -l`. + +If you get a message to the effect that your agent is not running, type ``eval `ssh-agent` `` and then `ssh-add -l`. +You should see something like `2048 SHA256:bII5VsQY3fCWXEai0lUeChEYPaagMXun3nB9U2eoUEM /home/betz4871/.ssh/raddit-user (RSA)`. If you do not, re-issue the command `ssh-add ~/.ssh/raddit-user` and re-confirm with `ssh-add -l`. + + Connect to the VM via SSH: ```bash $ ssh raddit-user@${INSTANCE_IP} From 1ff26cf4095e359f3c25714ee56596affed0086c Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sat, 4 Apr 2020 14:21:16 -0500 Subject: [PATCH 52/93] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e11da43..1c21436 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,9 @@ The target audience for this tutorial is anyone who loves or/and works in IT. ## Results of completing the tutorial -By the end of this tutorial, you'll make your own repository looking like [this one](https://github.com/Artemmkin/infrastructure-as-code-example). +By the end of this tutorial, you'll make your own repository looking like [this one](https://github.com/dm-academy/iac-tutorial-rsrc). -NOTE: you can use this [example repository](https://github.com/Artemmkin/infrastructure-as-code-example) in case you get stuck in some of the labs. +NOTE: you can use this [example repository](https://github.com/dm-academy/iac-tutorial-rsrc) in case you get stuck in some of the labs. ## Labs From e389cacb028ae67f50e0bc0605900251e8b263fc Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sat, 4 Apr 2020 14:23:42 -0500 Subject: [PATCH 53/93] Update 02-manual-operations.md --- docs/02-manual-operations.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/02-manual-operations.md b/docs/02-manual-operations.md index 615ae32..be11954 100644 --- a/docs/02-manual-operations.md +++ b/docs/02-manual-operations.md @@ -154,10 +154,13 @@ Verify that it's running: ```bash $ sudo systemctl status raddit ``` +Exit the VM. + +`$ exit` ## Access the Application -Open a firewall port the application is listening on (note that the following command should be run on your local machine): +Open a firewall port the application is listening on (note that the following command should be run on the Google Cloud Shell): ```bash $ gcloud compute firewall-rules create allow-raddit-tcp-9292 \ From 573623a8ec793de24eaf9a8d73e113b58eb6b00b Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sat, 4 Apr 2020 14:27:02 -0500 Subject: [PATCH 54/93] Update 02-manual-operations.md --- docs/02-manual-operations.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/02-manual-operations.md b/docs/02-manual-operations.md index be11954..b797ee0 100644 --- a/docs/02-manual-operations.md +++ b/docs/02-manual-operations.md @@ -187,10 +187,11 @@ Congrats! You've just deployed your application. It is running on a dedicated se Now that you've got the idea of what sort of steps you have to take to deploy your code from your local machine to a virtual server running in the cloud, let's see how we can do it more efficiently. -Destroy the current VM and move to the next step: +Destroy the current VM and firewall rule and move to the next step: ```bash -$ gcloud compute instances delete raddit-instance-2 +$ gcloud compute instances delete -q raddit-instance-2 +$ gcloud compute firewall-rules delete -q allow-raddit-tcp-9292 ``` Next: [Scripts](03-scripts.md) From 5928689f249e2ccdc0adc33b39cbb795b3984ca0 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sat, 4 Apr 2020 14:31:01 -0500 Subject: [PATCH 55/93] Update 03-scripts.md --- docs/03-scripts.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/03-scripts.md b/docs/03-scripts.md index d99404b..1c1f2ad 100644 --- a/docs/03-scripts.md +++ b/docs/03-scripts.md @@ -8,7 +8,7 @@ Now think about what happens if your application becomes so popular that one vir In all of these cases we face the task of provisioning new virtual machines, installing the required software and repeating all of the configurations we've made in the previous lab over and over again. -Doing it manually is `boring`, `error-prone` and `time-consuming`. +Doing it manually is boring, error-prone and time-consuming. The most obvious way for improvement is using Bash scripts which allow us to run sets of commands put in a single file. So let's try this. @@ -144,6 +144,13 @@ $ gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute i Save and commit the scripts created in this lab into your `iac-tutorial` repo. +Destroy the current VM and firewall rule and move to the next step: + +```bash +$ gcloud compute instances delete -q raddit-instance-3 +$ gcloud compute firewall-rules delete -q allow-raddit-tcp-9292 +``` + ## Conclusion Scripts helped us to save some time and effort of manually running every command one by one to configure the system and start the application. From 42d5574137b55eaf75ab991844e3add6655d4931 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sat, 4 Apr 2020 14:49:13 -0500 Subject: [PATCH 56/93] Update 03-scripts.md --- docs/03-scripts.md | 90 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 67 insertions(+), 23 deletions(-) diff --git a/docs/03-scripts.md b/docs/03-scripts.md index 1c1f2ad..e4e44c4 100644 --- a/docs/03-scripts.md +++ b/docs/03-scripts.md @@ -12,49 +12,93 @@ Doing it manually is boring, error-prone and time-consuming. The most obvious way for improvement is using Bash scripts which allow us to run sets of commands put in a single file. So let's try this. -## Provision Compute Resources +## Infrastructure as Code project + +Starting from this lab, we're going to use a git repo for saving all the work done in this tutorial. + +Go to your Github account and create a new repository called my-iac-repo. No README or .gitignore. Copy the URL. -Start a new VM for this lab. The command should look familiar: +Clone locally: ```bash -$ gcloud compute instances create raddit-instance-3 \ - --image-family ubuntu-1604-lts \ - --image-project ubuntu-os-cloud \ - --boot-disk-size 10GB \ - --machine-type n1-standard-1 +$ git clone ``` -## Infrastructure as Code project +Create a directory for this lab: -Starting from this lab, we're going to use a git repo for saving all the work done in this tutorial. +```bash +$ cd my-iac-repo +$ mkdir script +``` -Download a repo for the tutorial: +To push your changes up to Github: ```bash -$ git clone https://github.com/Artemmkin/iac-tutorial.git +$ git add . -A +$ git commit -m "some message" +$ git push origin master ``` -Delete git information about a remote repository: +Always issue these commands several times during each session. + +## Provisioning script + +We can automate the process of creating the VM and the firewall rule. + +In the `script` directory create a directory `provision`. + +Create a script `provision.sh`: + ```bash -$ cd ./iac-tutorial -$ git remote remove origin +#!/bin/bash +# add new VM +gcloud compute instances create raddit-instance-3 \ + --image-family ubuntu-1604-lts \ + --image-project ubuntu-os-cloud \ + --boot-disk-size 10GB \ + --machine-type n1-standard-1 + +# add firewall rule +gcloud compute firewall-rules create allow-raddit-tcp-9292 \ + --network default \ + --action allow \ + --direction ingress \ + --rules tcp:9292 \ + --source-ranges 0.0.0.0/0 ``` -Create a directory for this lab: +Run it in the Google Cloud Shell: ```bash -$ mkdir scripts +$ chmod +x provision/provision.sh +$ ./provision/provision.sh ``` +You should see results similar to: + +```bash +WARNING: You have selected a disk size of under [200GB]. This may result in poor I/O performance. For more information, see: https://developers.google.com/compute/docs/disks#performance. +Created [https://www.googleapis.com/compute/v1/projects/proven-sum-252123/zones/us-central1-c/instances/raddit-instance-3]. +NAME ZONE MACHINE_TYPE PREEMPTIBLE INTERNAL_IP EXTERNAL_IP STATUS +raddit-instance-3 us-central1-c n1-standard-1 10.128.0.58 34.71.103.20 RUNNING +Creating firewall...⠹Created [https://www.googleapis.com/compute/v1/projects/proven-sum-252123/global/firewalls/allow-raddit-tcp-9292]. +Creating firewall...done. +NAME NETWORK DIRECTION PRIORITY ALLOW DENY DISABLED +allow-raddit-tcp-9292 default INGRESS 1000 tcp:9292 False +``` + + ## Configuration script Before we can run our application, we need to create a running environment for it by installing dependent packages and configuring the OS. We are going to use the same commands we used before to do that, but this time, instead of running commands one by one, we'll create a `bash script` to save us some struggle. +In the `script` directory create a directory `config`. + Create a bash script to install Ruby, Bundler and MongoDB, and copy a systemd unit file for the application. -Save it to the `configuration.sh` file inside created `scripts` directory: +Save it to the `configuration.sh` file inside created `config` directory: ```bash #!/bin/bash @@ -84,7 +128,7 @@ mv raddit.service /etc/systemd/system/raddit.service Create a script for copying the application code from GitHub repository, installing dependent gems and starting it. -Save it into `deploy.sh` file inside `scripts` directory: +Save it into `deploy.sh` file inside `config` directory: ```bash #!/bin/bash @@ -104,11 +148,11 @@ sudo systemctl enable raddit ## Run the scripts -Copy the `scripts` directory to the created VM: +Copy the `config` directory to the created VM (you need to be in the `scripts` directory, or else make adjustments to your paths: ```bash $ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe raddit-instance-3) -$ scp -r ./scripts raddit-user@${INSTANCE_IP}:/home/raddit-user +$ scp -r ./config raddit-user@${INSTANCE_IP}:/home/raddit-user ``` NOTE: If you get an `offending ECDSA key` error, use the suggested removal command. @@ -125,9 +169,9 @@ $ ssh raddit-user@${INSTANCE_IP} Run the scripts: ```bash -$ chmod +x ./scripts/*.sh -$ sudo ./scripts/configuration.sh -$ ./scripts/deploy.sh +$ chmod +x ./config/*.sh +$ sudo ./config/configuration.sh +$ ./config/deploy.sh ``` ## Access the Application From 88b06934617155ca12cb878615ada0307c3753cc Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sat, 4 Apr 2020 14:51:37 -0500 Subject: [PATCH 57/93] Update 03-scripts.md --- docs/03-scripts.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/03-scripts.md b/docs/03-scripts.md index e4e44c4..e9e9734 100644 --- a/docs/03-scripts.md +++ b/docs/03-scripts.md @@ -184,17 +184,23 @@ Open another terminal and run the following command to get a public IP of the VM $ gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe raddit-instance-3 ``` -## Save and commit the work - -Save and commit the scripts created in this lab into your `iac-tutorial` repo. +## Destroy (de-provision) the resources by script -Destroy the current VM and firewall rule and move to the next step: +In the `provision` directory create a script `deprovision.sh`. ```bash -$ gcloud compute instances delete -q raddit-instance-3 -$ gcloud compute firewall-rules delete -q allow-raddit-tcp-9292 +#!/bin/bash +gcloud compute instances delete -q raddit-instance-3 +gcloud compute firewall-rules delete -q allow-raddit-tcp-9292 ``` +Set permissions correctly (see previous) and execute. + +## Save and commit the work + +Save and commit the scripts created in this lab into your `iac-tutorial` repo. + + ## Conclusion Scripts helped us to save some time and effort of manually running every command one by one to configure the system and start the application. From c78a957c543fefca5c9301b0856bac7683e1a63a Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sat, 4 Apr 2020 14:54:38 -0500 Subject: [PATCH 58/93] Update 03-scripts.md --- docs/03-scripts.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/03-scripts.md b/docs/03-scripts.md index e9e9734..742f4a3 100644 --- a/docs/03-scripts.md +++ b/docs/03-scripts.md @@ -194,7 +194,12 @@ gcloud compute instances delete -q raddit-instance-3 gcloud compute firewall-rules delete -q allow-raddit-tcp-9292 ``` -Set permissions correctly (see previous) and execute. +Set permissions correctly (see previous) and execute. You should get results like: + +```bash +Deleted [https://www.googleapis.com/compute/v1/projects/proven-sum-252123/zones/us-central1-c/instances/raddit-instance-3]. +Deleted [https://www.googleapis.com/compute/v1/projects/proven-sum-252123/global/firewalls/allow-raddit-tcp-9292]. +``` ## Save and commit the work From 85fd480ae61d98cdf91ff79c4b3979259b83cfe6 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sat, 4 Apr 2020 14:55:21 -0500 Subject: [PATCH 59/93] Update 03-scripts.md --- docs/03-scripts.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/03-scripts.md b/docs/03-scripts.md index 742f4a3..35de2ae 100644 --- a/docs/03-scripts.md +++ b/docs/03-scripts.md @@ -216,10 +216,5 @@ It's also a first step we've made in the direction of automating operations work But scripts are not suitable for every operations task and have many downsides. We'll discuss more on that in the next labs. -Destroy the current VM before moving onto the next step: - -```bash -$ gcloud compute instances delete raddit-instance-3 -``` Next: [Packer](04-packer.md) From 1d542071a5066d96d2286f4d8280d89c8ff2bf9f Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sat, 4 Apr 2020 14:59:15 -0500 Subject: [PATCH 60/93] Update 04-packer.md --- docs/04-packer.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/04-packer.md b/docs/04-packer.md index 83f045f..3182745 100644 --- a/docs/04-packer.md +++ b/docs/04-packer.md @@ -126,11 +126,11 @@ $ gcloud compute instances create raddit-instance-4 \ ## Deploy Application -Copy `deploy.sh` script to the created VM: +Copy `deploy.sh` script to the created VM (be careful of the path in the second command, be sure of your context): ```bash $ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe raddit-instance-4) -$ scp ./scripts/deploy.sh raddit-user@${INSTANCE_IP}:/home/raddit-user +$ scp ./scripts/config/deploy.sh raddit-user@${INSTANCE_IP}:/home/raddit-user ``` NOTE: If you get an offending ECDSA key error, use the suggested removal command. @@ -160,6 +160,15 @@ $ ./deploy.sh ## Access Application +Manually re-create the firewall rule: + +gcloud compute firewall-rules create allow-raddit-tcp-9292 \ + --network default \ + --action allow \ + --direction ingress \ + --rules tcp:9292 \ + --source-ranges 0.0.0.0/0 + Access the application in your browser by its public IP (don't forget to specify the port 9292). Open another terminal and run the following command to get a public IP of the VM: @@ -193,10 +202,11 @@ The advantages of its usage are quite obvious: * `It requires less time and effort to configure a new VM for running the application` * `System configuration becomes more reliable.` When we start a new VM to deploy the application, we know for sure that it has the right packages installed and configured properly, since we built and tested the image. -Destroy the current VM and move onto the next lab: +Destroy the current VM and rirewall rule and move onto the next lab: ```bash $ gcloud compute instances delete raddit-instance-4 +$ gcloud compute firewall-rules delete -q allow-raddit-tcp-9292 ``` Next: [Terraform](05-terraform.md) From 209efa6188ee1ee16689589ceb83645d2fbbcf31 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sat, 4 Apr 2020 15:01:55 -0500 Subject: [PATCH 61/93] Update 04-packer.md --- docs/04-packer.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/04-packer.md b/docs/04-packer.md index 3182745..20d8027 100644 --- a/docs/04-packer.md +++ b/docs/04-packer.md @@ -16,7 +16,7 @@ Luckily, we can create custom machine images with required configuration and sof [Download](https://www.packer.io/downloads.html) and install Packer onto your system (this means the Google Cloud Shell). -If you have issues, consult [this gist](https://gist.github.com/CharlesTBetz/20daf92169689e572873439439e1ad4f). +If you have issues, consult [this script](https://github.com/dm-academy/iac-tutorial-rsrc/blob/master/packer/install-packer.sh). Check the version to verify that it was installed: From 6757adf3ccf93682d243057265b5e19d41eaefe9 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sat, 4 Apr 2020 15:04:28 -0500 Subject: [PATCH 62/93] Update 04-packer.md --- docs/04-packer.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/04-packer.md b/docs/04-packer.md index 20d8027..14ccdf5 100644 --- a/docs/04-packer.md +++ b/docs/04-packer.md @@ -162,13 +162,15 @@ $ ./deploy.sh Manually re-create the firewall rule: -gcloud compute firewall-rules create allow-raddit-tcp-9292 \ +```bash +$ gcloud compute firewall-rules create allow-raddit-tcp-9292 \ --network default \ --action allow \ --direction ingress \ --rules tcp:9292 \ --source-ranges 0.0.0.0/0 - +``` + Access the application in your browser by its public IP (don't forget to specify the port 9292). Open another terminal and run the following command to get a public IP of the VM: @@ -177,6 +179,12 @@ Open another terminal and run the following command to get a public IP of the VM $ gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe raddit-instance-4 ``` +## De-provision +```bash +$ gcloud compute instances delete -q raddit-instance-4 +$ gcloud compute firewall-rules delete -q allow-raddit-tcp-9292 +``` + ## Save and commit the work Save and commit the packer template created in this lab into your `iac-tutorial` repo. @@ -202,11 +210,5 @@ The advantages of its usage are quite obvious: * `It requires less time and effort to configure a new VM for running the application` * `System configuration becomes more reliable.` When we start a new VM to deploy the application, we know for sure that it has the right packages installed and configured properly, since we built and tested the image. -Destroy the current VM and rirewall rule and move onto the next lab: - -```bash -$ gcloud compute instances delete raddit-instance-4 -$ gcloud compute firewall-rules delete -q allow-raddit-tcp-9292 -``` Next: [Terraform](05-terraform.md) From 21eeffea47fe22dc16fd641dd8200591349f9fc3 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sat, 4 Apr 2020 15:06:55 -0500 Subject: [PATCH 63/93] Update 05-terraform.md --- docs/05-terraform.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/05-terraform.md b/docs/05-terraform.md index a85ed92..9a8282a 100644 --- a/docs/05-terraform.md +++ b/docs/05-terraform.md @@ -162,6 +162,17 @@ $ ./deploy.sh ## Access the Application +Manually create the firewall rule: + +```bash +$ gcloud compute firewall-rules create allow-raddit-tcp-9292 \ + --network default \ + --action allow \ + --direction ingress \ + --rules tcp:9292 \ + --source-ranges 0.0.0.0/0 +``` + Access the application in your browser by its public IP (don't forget to specify the port 9292). Open another terminal and run the following command to get a public IP of the VM: @@ -172,9 +183,7 @@ $ gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute i ## Add other GCP resources into Terraform -Do you remember how in previous labs we created some GCP resources like SSH project keys and a firewall rule for our application via `gcloud` tool? - -Let's add those into our Terraform configuration so that we know for sure those resources are present. +Let's add ssh keys and the firewall rule into our Terraform configuration so that we know for sure those resources are present. First, delete the SSH project key and firewall rule: From 1ad25db74e0d31421a6fab77198075b99c5205fa Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sat, 4 Apr 2020 15:21:46 -0500 Subject: [PATCH 64/93] Update 08-docker.md --- docs/08-docker.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/08-docker.md b/docs/08-docker.md index 6593c11..4adbfa0 100644 --- a/docs/08-docker.md +++ b/docs/08-docker.md @@ -45,7 +45,7 @@ Dockerfile contains `instructions` on how the image should be built. Here are so Let's use these instructions to create a Docker container image for our raddit application. -Create a folder called `docker` inside your `iac-tutorial` repo. Inside of that new directory, create a text file called `Dockerfile` with the following content: +Inside your `my-iac-tutorial` directory, create a text file called `Dockerfile` with the following content: ``` # Use base image with Ruby installed @@ -104,7 +104,7 @@ CMD ["puma"] ## Build Container Image -Once you defined how your image should be built, run the following command inside `docker` directory to create a container image for raddit application: +Once you defined how your image should be built, run the following command inside your `my-iac-tutorial` directory to create a container image for raddit application: ```bash $ docker build --tag raddit . From 8fef76faa6e90fd2d6df2f4b50fd7608bd539c54 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sat, 4 Apr 2020 15:35:20 -0500 Subject: [PATCH 65/93] Update 09-docker-compose.md --- docs/09-docker-compose.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/09-docker-compose.md b/docs/09-docker-compose.md index 288612d..0659125 100644 --- a/docs/09-docker-compose.md +++ b/docs/09-docker-compose.md @@ -135,7 +135,7 @@ $ docker-compose up -d ## Access Application -The application should be accessible to your as before at http://localhost:9292 +The application should be accessible to your as before via the web preview icon in Google Cloud Shell. `curl localhost:9292` will at least dump out the HTML (not very pretty, but if you see HTML you know the service is working to some degree at least). ## Save and commit the work From b641a6cb854a2009e0bc5518fe41c6137b97b09b Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sat, 4 Apr 2020 15:38:47 -0500 Subject: [PATCH 66/93] Update 10-kubernetes.md --- docs/10-kubernetes.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/10-kubernetes.md b/docs/10-kubernetes.md index 00571e1..bc7b4b8 100644 --- a/docs/10-kubernetes.md +++ b/docs/10-kubernetes.md @@ -44,7 +44,7 @@ Create a new directory called `kubernetes` inside your `iac-tutorial` repo, whic ## Describe Kubernetes cluster in Terraform -We'll use [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/) (GKE) service to deploy a Kubernetes cluster of 2 nodes. +We'll use [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/) (GKE) service to deploy a Kubernetes cluster of 3 nodes. We'll describe a Kubernetes cluster using Terraform so that we can manage it through code. @@ -75,10 +75,6 @@ variable "cluster_name" { variable "zone" { description = "The zone in which nodes specified in initial_node_count should be created in" } -© 2020 GitHub, Inc. -Terms -Privacy - ``` ### terraform.tfvars From 1fc4be70fdb7928d8c577c0ea929766e6087a0fc Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sat, 4 Apr 2020 15:40:31 -0500 Subject: [PATCH 67/93] Update 10-kubernetes.md --- docs/10-kubernetes.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/10-kubernetes.md b/docs/10-kubernetes.md index bc7b4b8..91e3435 100644 --- a/docs/10-kubernetes.md +++ b/docs/10-kubernetes.md @@ -80,12 +80,12 @@ variable "zone" { ### terraform.tfvars ```bash // define provider configuration variables -project_id = "proven-sum-252123" # project in which to create a cluster -region = "us-central1" # region in which to create a cluster +project_id = "some-project-ID" # project in which to create a cluster +region = "some-google-region" # region in which to create a cluster // define Kubernetes cluster variables cluster_name = "iac-tutorial-cluster" # cluster name -zone = "us-central1-c" # zone in which to create a cluster nodes +zone = "some-google-zone" # zone in which to create a cluster nodes ``` @@ -149,7 +149,7 @@ We'll use this Terraform code to create a Kubernetes cluster. ## Create Kubernetes Cluster -`main.tf` which you downloaded holds all the information about the cluster that should be created. It's parameterized using Terraform [input variables](https://www.terraform.io/intro/getting-started/variables.html) which allow you to easily change configuration parameters. +`main.tf` holds all the information about the cluster that should be created. It's parameterized using Terraform [input variables](https://www.terraform.io/intro/getting-started/variables.html) which allow you to easily change configuration parameters. Look into `terraform.tfvars` file which contains definitions of the input variables and change them if necessary. You'll most probably want to change `project_id` value. @@ -162,6 +162,7 @@ region = "europe-west1" # region in which to create a clus cluster_name = "iac-tutorial-cluster" # cluster name zone = "europe-west1-b" # zone in which to create a cluster nodes ``` + After you've defined the variables, run Terraform inside `kubernetes/terraform` to create a Kubernetes cluster consisting of 2 nodes (VMs for running our application containers). ```bash From 9181a552eb00e8a84d853d3301e3a9a1087e8c6f Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sat, 4 Apr 2020 15:41:55 -0500 Subject: [PATCH 68/93] Update 10-kubernetes.md --- docs/10-kubernetes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/10-kubernetes.md b/docs/10-kubernetes.md index 91e3435..e314eeb 100644 --- a/docs/10-kubernetes.md +++ b/docs/10-kubernetes.md @@ -217,7 +217,7 @@ spec: spec: containers: - name: raddit - image: artemkin/raddit + image: raddit env: - name: DATABASE_HOST value: mongo-service From 4b97c21a5caa0d5e5243b4b8bf4e114ea1bffee1 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sat, 4 Apr 2020 16:32:51 -0500 Subject: [PATCH 69/93] Update 10-kubernetes.md --- docs/10-kubernetes.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/10-kubernetes.md b/docs/10-kubernetes.md index e314eeb..b25b8f3 100644 --- a/docs/10-kubernetes.md +++ b/docs/10-kubernetes.md @@ -217,7 +217,7 @@ spec: spec: containers: - name: raddit - image: raddit + image: dmacademy/raddit env: - name: DATABASE_HOST value: mongo-service @@ -276,7 +276,7 @@ In the Pod object definition (`Pod template`) we specify container information s spec: containers: - name: raddit - image: artemkin/raddit + image: dmacademy/raddit env: - name: DATABASE_HOST value: mongo-service @@ -284,7 +284,9 @@ In the Pod object definition (`Pod template`) we specify container information s Notice how we also pass an environment variable to the container. `DATABASE_HOST` variable tells our application how to contact the database. We define `mongo-service` as its value to specify the name of the Kubernetes service to contact (more about the Services will be in the next section). -Container images will be downloaded from Docker Hub in this case. +Container images will be downloaded from Docker Hub in this case: the generic mongo container and the raddit image uploaded to the dmacademy organization. + +*It would be nice if we could use the locally built raddit image. Extra credit for anyone who can figure out how to do that.* ## Create Deployment Objects From 6dc85022d7d6b5bdca094c50a9fbefcb7e9bc200 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Fri, 24 Apr 2020 19:28:47 -0500 Subject: [PATCH 70/93] Update 02-manual-operations.md --- docs/02-manual-operations.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/02-manual-operations.md b/docs/02-manual-operations.md index b797ee0..6925786 100644 --- a/docs/02-manual-operations.md +++ b/docs/02-manual-operations.md @@ -41,6 +41,12 @@ $ gcloud compute project-info add-metadata \ --metadata ssh-keys="raddit-user:$(cat ~/.ssh/raddit-user.pub)" ``` +Run your ssh-agent is running: + +```bash +$ eval `ssh-agent` +``` + Add the SSH private key to the ssh-agent: ``` From 6f5e8c2c7cf366e698b3800a43e5840bc1c16f40 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Fri, 24 Apr 2020 19:35:36 -0500 Subject: [PATCH 71/93] Update 02-manual-operations.md --- docs/02-manual-operations.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/02-manual-operations.md b/docs/02-manual-operations.md index 6925786..2f0afae 100644 --- a/docs/02-manual-operations.md +++ b/docs/02-manual-operations.md @@ -41,7 +41,12 @@ $ gcloud compute project-info add-metadata \ --metadata ssh-keys="raddit-user:$(cat ~/.ssh/raddit-user.pub)" ``` -Run your ssh-agent is running: +Check your ssh-agent is running: + +```bash +$ echo $SSH_AGENT_PID +``` +If you get a number, it is running. If you get nothing, then run: ```bash $ eval `ssh-agent` From 76225118508ed401967ae4ec681f690e8d16a1d2 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Mon, 18 May 2020 10:42:04 -0500 Subject: [PATCH 72/93] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1c21436..7c5a608 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![license](https://img.shields.io/github/license/Artemmkin/infrastructure-as-code-tutorial.svg)](https://github.com/Artemmkin/infrastructure-as-code-tutorial/blob/master/LICENSE) [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Learn%20about%20Infrastructure%20as%20Code%20https%3A%2F%2Fgithub.com%2FArtemmkin%2Finfrastructure-as-code-tutorial%20%20Tutorial%20created%20by%20@artemmkins%20covers%20%23Packer,%20%23Terraform,%20%23Ansible,%20%23Vagrant,%20%23Docker,%20and%20%23Kubernetes.%20%23DevOps) -This tutorial is intended to show what the **Infrastructure as Code** (**IaC**) is, why we need it, and how it can help you manage your infrastructure more efficiently. +(Intro by original author of this material) This tutorial is intended to show what the **Infrastructure as Code** (**IaC**) is, why we need it, and how it can help you manage your infrastructure more efficiently. It is practice-based, meaning I don't give much theory on what Infrastructure as Code is in the beginning of the tutorial, but instead let you feel it through the practice first. At the end of the tutorial, I summarize some of the key points about Infrastructure as Code based on what you learn through the labs. From ddeae2734a913fa512fd9a3238014a0a968a3604 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sun, 9 Aug 2020 20:15:22 -0500 Subject: [PATCH 73/93] Update README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 7c5a608..c68ac9d 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,7 @@ The target audience for this tutorial is anyone who loves or/and works in IT. ## Results of completing the tutorial -By the end of this tutorial, you'll make your own repository looking like [this one](https://github.com/dm-academy/iac-tutorial-rsrc). - -NOTE: you can use this [example repository](https://github.com/dm-academy/iac-tutorial-rsrc) in case you get stuck in some of the labs. +By the end of this tutorial, you'll make your own repository looking like [this one](https://github.com/dm-academy/iac-tutorial-rsrc). Feel free to inspect it in case you get stuck in some of the labs. ## Labs From 64b775684a520d3575eebc6b3e89ea7b9afc9989 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Sun, 9 Aug 2020 20:19:29 -0500 Subject: [PATCH 74/93] Update 02-manual-operations.md --- docs/02-manual-operations.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/02-manual-operations.md b/docs/02-manual-operations.md index 2f0afae..69227e3 100644 --- a/docs/02-manual-operations.md +++ b/docs/02-manual-operations.md @@ -6,6 +6,8 @@ To better understand the `Infrastructure as Code` (`IaC`) concept, we will first Imagine you have developed a new cool application called [raddit](https://github.com/Artemmkin/raddit). +_This is where I left off 8/9. Need to convert to node-svc_ + You want to run your application on a dedicated server and make it available to the Internet users. You heard about the `public cloud` thing, which allows you to provision compute resources and pay only for what you use. You believe it's a great way to test your idea of an application and see if people like it. From 6beee9f552db5abc43d1e979d2e1c34224b128b0 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Mon, 10 Aug 2020 19:32:59 -0500 Subject: [PATCH 75/93] Update 02-manual-operations.md --- docs/02-manual-operations.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/02-manual-operations.md b/docs/02-manual-operations.md index 69227e3..0735da5 100644 --- a/docs/02-manual-operations.md +++ b/docs/02-manual-operations.md @@ -4,9 +4,7 @@ To better understand the `Infrastructure as Code` (`IaC`) concept, we will first ## Intro -Imagine you have developed a new cool application called [raddit](https://github.com/Artemmkin/raddit). - -_This is where I left off 8/9. Need to convert to node-svc_ +Imagine you have developed a new cool application called [node-svc](https://github.com/dm-academy/node-svc-v1). You want to run your application on a dedicated server and make it available to the Internet users. From 223ea277ae94e8ae493e10017b473be9f1bccae4 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Mon, 10 Aug 2020 19:37:35 -0500 Subject: [PATCH 76/93] Update 02-manual-operations.md --- docs/02-manual-operations.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/02-manual-operations.md b/docs/02-manual-operations.md index 0735da5..c1d01ca 100644 --- a/docs/02-manual-operations.md +++ b/docs/02-manual-operations.md @@ -19,11 +19,11 @@ First thing we will do is to provision a virtual machine (VM) inside GCP for run Use the following gcloud command in your terminal to launch a VM with Ubuntu 16.04 distro: ```bash -$ gcloud compute instances create raddit-instance-2 \ +$ gcloud compute instances create node-svc-instance \ --image-family ubuntu-1604-lts \ --image-project ubuntu-os-cloud \ - --boot-disk-size 10GB \ - --machine-type n1-standard-1 + --boot-disk-size 1GB \ + --machine-type f1-micro ``` ## Create an SSH key pair From 773264ab1ab5b7e0876694ba2b5fb4783f0cd54c Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Mon, 10 Aug 2020 19:38:19 -0500 Subject: [PATCH 77/93] Update 02-manual-operations.md --- docs/02-manual-operations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/02-manual-operations.md b/docs/02-manual-operations.md index c1d01ca..9176902 100644 --- a/docs/02-manual-operations.md +++ b/docs/02-manual-operations.md @@ -22,7 +22,7 @@ Use the following gcloud command in your terminal to launch a VM with Ubuntu 16. $ gcloud compute instances create node-svc-instance \ --image-family ubuntu-1604-lts \ --image-project ubuntu-os-cloud \ - --boot-disk-size 1GB \ + --boot-disk-size 10GB \ --machine-type f1-micro ``` From 6b61ccf507237b86f54bde8f9014deccea23e89e Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Mon, 10 Aug 2020 19:40:06 -0500 Subject: [PATCH 78/93] Update 02-manual-operations.md --- docs/02-manual-operations.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/02-manual-operations.md b/docs/02-manual-operations.md index 9176902..4064dad 100644 --- a/docs/02-manual-operations.md +++ b/docs/02-manual-operations.md @@ -31,14 +31,14 @@ $ gcloud compute instances create node-svc-instance \ Generate an SSH key pair for future connections to the VM instances (run the command exactly as it is): ```bash -$ ssh-keygen -t rsa -f ~/.ssh/raddit-user -C raddit-user -P "" +$ ssh-keygen -t rsa -f ~/.ssh/raddit-user -C node-user -P "" ``` Create an SSH public key for your project: ```bash $ gcloud compute project-info add-metadata \ - --metadata ssh-keys="raddit-user:$(cat ~/.ssh/raddit-user.pub)" + --metadata ssh-keys="node-user:$(cat ~/.ssh/node-user.pub)" ``` Check your ssh-agent is running: @@ -55,7 +55,7 @@ $ eval `ssh-agent` Add the SSH private key to the ssh-agent: ``` -$ ssh-add ~/.ssh/raddit-user +$ ssh-add ~/.ssh/node-user ``` Verify that the key was added to the ssh-agent: From 88c58a8aae39c03464db12365d6dbcccb9ff4d4b Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Mon, 10 Aug 2020 19:40:44 -0500 Subject: [PATCH 79/93] Update 02-manual-operations.md --- docs/02-manual-operations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/02-manual-operations.md b/docs/02-manual-operations.md index 4064dad..a4c893b 100644 --- a/docs/02-manual-operations.md +++ b/docs/02-manual-operations.md @@ -31,7 +31,7 @@ $ gcloud compute instances create node-svc-instance \ Generate an SSH key pair for future connections to the VM instances (run the command exactly as it is): ```bash -$ ssh-keygen -t rsa -f ~/.ssh/raddit-user -C node-user -P "" +$ ssh-keygen -t rsa -f ~/.ssh/node-user -C node-user -P "" ``` Create an SSH public key for your project: From 77ced44946bf33431ead8710e0c46bafae415480 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Mon, 10 Aug 2020 19:54:50 -0500 Subject: [PATCH 80/93] Update 02-manual-operations.md --- docs/02-manual-operations.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/02-manual-operations.md b/docs/02-manual-operations.md index a4c893b..8943930 100644 --- a/docs/02-manual-operations.md +++ b/docs/02-manual-operations.md @@ -71,8 +71,8 @@ To start the application, you need to first configure the environment for runnin Connect to the started VM via SSH using the following two commands: ```bash -$ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe raddit-instance-2) -$ ssh raddit-user@${INSTANCE_IP} +$ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc-instance) +$ ssh node-user@${INSTANCE_IP} ``` Install Ruby: From 92b48572b37ba1158d431dfc47285a5f02a50384 Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Mon, 10 Aug 2020 19:57:01 -0500 Subject: [PATCH 81/93] Update 02-manual-operations.md --- docs/02-manual-operations.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/02-manual-operations.md b/docs/02-manual-operations.md index 8943930..3ad7811 100644 --- a/docs/02-manual-operations.md +++ b/docs/02-manual-operations.md @@ -75,6 +75,8 @@ $ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].nat $ ssh node-user@${INSTANCE_IP} ``` + +_left off here_ Install Ruby: ```bash From 7b710b1482ac7acb413fdc5c24c0459914264389 Mon Sep 17 00:00:00 2001 From: Charles Betz Date: Tue, 11 Aug 2020 19:47:44 -0500 Subject: [PATCH 82/93] finished 02 first cut --- .DS_Store | Bin 6148 -> 6148 bytes docs/02-manual-operations.md | 95 +++++++++++------------------------ 2 files changed, 28 insertions(+), 67 deletions(-) diff --git a/.DS_Store b/.DS_Store index 2e97c10d5cd61d82bb3267d87d58d86009649a28..1ea5da383e5b57f30390774b08f6a747a3697627 100644 GIT binary patch delta 52 zcmZoMXfc@J&&ahgU^gQp*JK{1I diff --git a/docs/02-manual-operations.md b/docs/02-manual-operations.md index 3ad7811..22a8ca1 100644 --- a/docs/02-manual-operations.md +++ b/docs/02-manual-operations.md @@ -77,122 +77,83 @@ $ ssh node-user@${INSTANCE_IP} _left off here_ -Install Ruby: +Install Node and npm: ```bash $ sudo apt-get update -$ sudo apt-get install -y ruby-full build-essential +$ sudo apt-get install nodejs npm ``` -Check the installed version of Ruby: +Check the installed version of Node: ```bash -$ ruby -v +$ nodejs -v ``` -Install Bundler: - -```bash -$ sudo gem install --no-rdoc --no-ri bundler -$ bundle version -``` - -Clone the [application repo](https://github.com/Artemmkin/raddit), but first make sure `git` is installed: -```bash -$ git version -``` - -At the time of writing the latest image of Ubuntu 16.04 which GCP provides has `git` preinstalled, so we can skip this step. - -Clone the application repo into the home directory of `raddit-user` user: - -```bash -$ git clone https://github.com/Artemmkin/raddit.git -``` - -Install application dependencies using Bundler: - +Install `git`: ```bash -$ cd ./raddit -$ sudo bundle install +$ sudo apt install git ``` -## Prepare Database - -Install MongoDB which your application uses: +Clone the application repo into the home directory of `node-user` user (reminder, how do you clone to the right location?): ```bash -$ sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv EA312927 -$ echo "deb http://repo.mongodb.org/apt/ubuntu xenial/mongodb-org/3.2 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.2.list -$ sudo apt-get update -$ sudo apt-get install -y mongodb-org --allow-unauthenticated +$ git clone https://github.com/dm-academy/node-svc-v1 ``` - -Start MongoDB and enable autostart: +Navigate to the repo and check out the 02 branch (matching this lesson) ```bash -$ sudo systemctl start mongod -$ sudo systemctl enable mongod +$ git checkout 02 +Branch 02 set up to track remote branch 02 from origin. +Switched to a new branch '02' ``` -Verify that MongoDB is running: +Initialize npm (Node Package Manager) and install express: ```bash -$ sudo systemctl status mongod +$ npm install +$ npm install express ``` ## Start the Application -Download a systemd unit file for starting the application from a gist: - -```bash -$ wget https://gist.githubusercontent.com/Artemmkin/ce82397cfc69d912df9cd648a8d69bec/raw/7193a36c9661c6b90e7e482d256865f085a853f2/raddit.service -``` - -Move it to the systemd directory - -```bash -$ sudo mv raddit.service /etc/systemd/system/raddit.service -``` - -Now start the application and enable autostart: +Start the Node web server: ```bash -$ sudo systemctl start raddit -$ sudo systemctl enable raddit +$ nodejs server.js & +Running on 3000 ``` -Verify that it's running: +Test it: ```bash -$ sudo systemctl status raddit +$ curl localhost:3000 +Successful request. ``` -Exit the VM. -`$ exit` ## Access the Application Open a firewall port the application is listening on (note that the following command should be run on the Google Cloud Shell): ```bash -$ gcloud compute firewall-rules create allow-raddit-tcp-9292 \ +$ gcloud compute firewall-rules create allow-node-svc-tcp-3000 \ --network default \ --action allow \ --direction ingress \ - --rules tcp:9292 \ + --rules tcp:3000 \ --source-ranges 0.0.0.0/0 ``` Get the public IP of the VM: ```bash -$ gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe raddit-instance-2 +$ gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc-instance ``` -Now open your browser and try to reach the application at the public IP and port 9292. +Now open your browser and try to reach the application at the public IP and port 3000. -For example, I put in my browser the following URL http://104.155.1.152:9292, but note that you'll have your own IP address. +For example, I put in my browser the following URL http://104.155.1.152:3000, but note that you'll have your own IP address. ## Conclusion @@ -203,8 +164,8 @@ Now that you've got the idea of what sort of steps you have to take to deploy yo Destroy the current VM and firewall rule and move to the next step: ```bash -$ gcloud compute instances delete -q raddit-instance-2 -$ gcloud compute firewall-rules delete -q allow-raddit-tcp-9292 +$ gcloud compute instances delete -q node-svc-instance +$ gcloud compute firewall-rules delete -q allow-node-svc-tcp-9292 ``` Next: [Scripts](03-scripts.md) From af879480cda4b99b6c3c937689f8b71e17aa618f Mon Sep 17 00:00:00 2001 From: "Charles T. Betz" Date: Tue, 11 Aug 2020 19:50:46 -0500 Subject: [PATCH 83/93] Update 02-manual-operations.md --- docs/02-manual-operations.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/02-manual-operations.md b/docs/02-manual-operations.md index 22a8ca1..18b0c42 100644 --- a/docs/02-manual-operations.md +++ b/docs/02-manual-operations.md @@ -75,8 +75,6 @@ $ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].nat $ ssh node-user@${INSTANCE_IP} ``` - -_left off here_ Install Node and npm: ```bash From 8b2d03e7704a956c3669c1a0dfb240fca1a6b906 Mon Sep 17 00:00:00 2001 From: Charles Betz Date: Sat, 15 Aug 2020 11:05:21 -0500 Subject: [PATCH 84/93] updating 02 --- docs/02-manual-operations.md | 8 +-- docs/03-scripts.md | 101 +++++++++++++---------------------- 2 files changed, 42 insertions(+), 67 deletions(-) diff --git a/docs/02-manual-operations.md b/docs/02-manual-operations.md index 18b0c42..0221245 100644 --- a/docs/02-manual-operations.md +++ b/docs/02-manual-operations.md @@ -19,7 +19,7 @@ First thing we will do is to provision a virtual machine (VM) inside GCP for run Use the following gcloud command in your terminal to launch a VM with Ubuntu 16.04 distro: ```bash -$ gcloud compute instances create node-svc-instance \ +$ gcloud compute instances create node-svc\ --image-family ubuntu-1604-lts \ --image-project ubuntu-os-cloud \ --boot-disk-size 10GB \ @@ -71,7 +71,7 @@ To start the application, you need to first configure the environment for runnin Connect to the started VM via SSH using the following two commands: ```bash -$ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc-instance) +$ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc) $ ssh node-user@${INSTANCE_IP} ``` @@ -115,6 +115,8 @@ $ npm install express ## Start the Application +Look at the server.js file (`cat`). We will discuss in class. + Start the Node web server: ```bash @@ -162,7 +164,7 @@ Now that you've got the idea of what sort of steps you have to take to deploy yo Destroy the current VM and firewall rule and move to the next step: ```bash -$ gcloud compute instances delete -q node-svc-instance +$ gcloud compute instances delete -q node-svc $ gcloud compute firewall-rules delete -q allow-node-svc-tcp-9292 ``` diff --git a/docs/03-scripts.md b/docs/03-scripts.md index 35de2ae..f0be15f 100644 --- a/docs/03-scripts.md +++ b/docs/03-scripts.md @@ -1,6 +1,6 @@ # Scripts -In the previous lab, you deployed the [raddit](https://github.com/Artemmkin/raddit) application by connecting to a VM via SSH and running commands in the terminal one by one. In this lab, we'll try to automate this process a little by using `scripts`. +In the previous lab, you deployed the [node-svc](https://github.com/dm-academy/node-svc) application by connecting to a VM via SSH and running commands in the terminal one by one. In this lab, we'll try to automate this process a little by using `scripts`. ## Intro @@ -28,14 +28,14 @@ Create a directory for this lab: ```bash $ cd my-iac-repo -$ mkdir script +$ mkdir 02-script ``` To push your changes up to Github: ```bash $ git add . -A -$ git commit -m "some message" +$ git commit -m "first lab 02 commit" # should be relevant to the changes you made $ git push origin master ``` @@ -45,126 +45,100 @@ Always issue these commands several times during each session. We can automate the process of creating the VM and the firewall rule. -In the `script` directory create a directory `provision`. - -Create a script `provision.sh`: +In the `script` directory create a script `provision.sh`: ```bash #!/bin/bash # add new VM -gcloud compute instances create raddit-instance-3 \ +gcloud compute instances create node-svc \ --image-family ubuntu-1604-lts \ --image-project ubuntu-os-cloud \ --boot-disk-size 10GB \ --machine-type n1-standard-1 # add firewall rule -gcloud compute firewall-rules create allow-raddit-tcp-9292 \ +gcloud compute firewall-rules create allow-node-svc-3000 \ --network default \ --action allow \ --direction ingress \ - --rules tcp:9292 \ + --rules tcp:3000 \ --source-ranges 0.0.0.0/0 ``` Run it in the Google Cloud Shell: ```bash -$ chmod +x provision/provision.sh -$ ./provision/provision.sh +$ chmod +x provision.sh # changing permissions +$ ./provision.sh # you have to include the './' ``` You should see results similar to: ```bash WARNING: You have selected a disk size of under [200GB]. This may result in poor I/O performance. For more information, see: https://developers.google.com/compute/docs/disks#performance. -Created [https://www.googleapis.com/compute/v1/projects/proven-sum-252123/zones/us-central1-c/instances/raddit-instance-3]. -NAME ZONE MACHINE_TYPE PREEMPTIBLE INTERNAL_IP EXTERNAL_IP STATUS -raddit-instance-3 us-central1-c n1-standard-1 10.128.0.58 34.71.103.20 RUNNING -Creating firewall...⠹Created [https://www.googleapis.com/compute/v1/projects/proven-sum-252123/global/firewalls/allow-raddit-tcp-9292]. +Created [https://www.googleapis.com/compute/v1/projects/proven-sum-252123/zones/us-central1-c/instances/node-svc]. +NAME ZONE MACHINE_TYPE PREEMPTIBLE INTERNAL_IP EXTERNAL_IP STATUS +node-svc us-central1-c n1-standard-1 10.128.15.202 34.69.206.6 RUNNING +Creating firewall...⠹Created [https://www.googleapis.com/compute/v1/projects/proven-sum-252123/global/firewalls/allow-node-svc-3000]. Creating firewall...done. -NAME NETWORK DIRECTION PRIORITY ALLOW DENY DISABLED -allow-raddit-tcp-9292 default INGRESS 1000 tcp:9292 False +NAME NETWORK DIRECTION PRIORITY ALLOW DENY DISABLED +allow-node-svc-3000 default INGRESS 1000 tcp:3000 False ``` - ## Configuration script Before we can run our application, we need to create a running environment for it by installing dependent packages and configuring the OS. We are going to use the same commands we used before to do that, but this time, instead of running commands one by one, we'll create a `bash script` to save us some struggle. -In the `script` directory create a directory `config`. - -Create a bash script to install Ruby, Bundler and MongoDB, and copy a systemd unit file for the application. - -Save it to the `configuration.sh` file inside created `config` directory: +In the `02-script` directory create a bash script `config.sh` to install node, npm, express, and git, and download the app and initialize node. ```bash #!/bin/bash -set -e +set -e # exit immediately if anything returns non-zero. See https://www.javatpoint.com/linux-set-command -echo " ----- install ruby and bundler ----- " +echo " ----- install node, npm, git ----- " apt-get update -apt-get install -y ruby-full build-essential -gem install --no-rdoc --no-ri bundler - -echo " ----- install mongodb ----- " -apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv EA312927 -echo "deb http://repo.mongodb.org/apt/ubuntu xenial/mongodb-org/3.2 multiverse" > /etc/apt/sources.list.d/mongodb-org-3.2.list -apt-get update -apt-get install -y mongodb-org --allow-unauthenticated - -echo " ----- start mongodb ----- " -systemctl start mongod -systemctl enable mongod - -echo " ----- copy unit file for application ----- " -wget https://gist.githubusercontent.com/Artemmkin/ce82397cfc69d912df9cd648a8d69bec/raw/7193a36c9661c6b90e7e482d256865f085a853f2/raddit.service -mv raddit.service /etc/systemd/system/raddit.service +apt-get install -y nodejs npm git ``` ## Deployment script -Create a script for copying the application code from GitHub repository, installing dependent gems and starting it. - -Save it into `deploy.sh` file inside `config` directory: +Create a script for copying the application code from GitHub repository, initializing NPM and downloading express.js, and starting the server. Save it as `deploy.sh` inside `02-script` directory: ```bash #!/bin/bash set -e -echo " ----- clone application repository ----- " -git clone https://github.com/Artemmkin/raddit.git - -echo " ----- install dependent gems ----- " -cd ./raddit -sudo bundle install +echo " ----- download, initialize, and run app ----- " +git clone https://github.com/dm-academy/node-svc-v1 +cd node-svc-v1 +git checkout 02 +npm install +npm install express +nodejs server.js & -echo " ----- start the application ----- " -sudo systemctl start raddit -sudo systemctl enable raddit ``` ## Run the scripts -Copy the `config` directory to the created VM (you need to be in the `scripts` directory, or else make adjustments to your paths: +Copy the `02-script` directory to the created VM: ```bash -$ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe raddit-instance-3) -$ scp -r ./config raddit-user@${INSTANCE_IP}:/home/raddit-user +$ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc) +$ scp -r . node-user@${INSTANCE_IP}:/home/node-user ``` NOTE: If you get an `offending ECDSA key` error, use the suggested removal command. NOTE: If you get the error `Permission denied (publickey).`, this probably means that your ssh-agent no longer has the raddit-user private key added. This easily happens if the Google Cloud Shell goes to sleep and wipes out your session. Check via issuing `ssh-add -l`. If you get a message to the effect that your agent is not running, type ``eval `ssh-agent` `` and then `ssh-add -l`. -You should see something like `2048 SHA256:bII5VsQY3fCWXEai0lUeChEYPaagMXun3nB9U2eoUEM /home/betz4871/.ssh/raddit-user (RSA)`. If you do not, re-issue the command `ssh-add ~/.ssh/raddit-user` and re-confirm with `ssh-add -l`. +You should see something like `2048 SHA256:bII5VsQY3fCWXEai0lUeChEYPaagMXun3nB9U2eoUEM /home/betz4871/.ssh/node-user (RSA)`. If you do not, re-issue the command `ssh-add ~/.ssh/node-user` and re-confirm with `ssh-add -l`. Connect to the VM via SSH: ```bash -$ ssh raddit-user@${INSTANCE_IP} +$ ssh node-user@${INSTANCE_IP} ``` Run the scripts: @@ -176,12 +150,12 @@ $ ./config/deploy.sh ## Access the Application -Access the application in your browser by its public IP (don't forget to specify the port 9292). +Access the application in your browser by its public IP (don't forget to specify the port 3000). Open another terminal and run the following command to get a public IP of the VM: ```bash -$ gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe raddit-instance-3 +$ gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-instance ``` ## Destroy (de-provision) the resources by script @@ -190,15 +164,14 @@ In the `provision` directory create a script `deprovision.sh`. ```bash #!/bin/bash -gcloud compute instances delete -q raddit-instance-3 -gcloud compute firewall-rules delete -q allow-raddit-tcp-9292 +gcloud compute instances delete -q node-instance +gcloud compute firewall-rules delete -q allow-node-tcp-3000 ``` Set permissions correctly (see previous) and execute. You should get results like: ```bash -Deleted [https://www.googleapis.com/compute/v1/projects/proven-sum-252123/zones/us-central1-c/instances/raddit-instance-3]. -Deleted [https://www.googleapis.com/compute/v1/projects/proven-sum-252123/global/firewalls/allow-raddit-tcp-9292]. +Xxxxxx ``` ## Save and commit the work From d9a3e2a9bddaf09513f1089ce420ddf3f1f7d2c7 Mon Sep 17 00:00:00 2001 From: Charles Betz Date: Sat, 15 Aug 2020 11:46:32 -0500 Subject: [PATCH 85/93] 02 work --- docs/03-scripts.md | 58 +++++++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/docs/03-scripts.md b/docs/03-scripts.md index f0be15f..d19fff9 100644 --- a/docs/03-scripts.md +++ b/docs/03-scripts.md @@ -16,7 +16,7 @@ The most obvious way for improvement is using Bash scripts which allow us to run Starting from this lab, we're going to use a git repo for saving all the work done in this tutorial. -Go to your Github account and create a new repository called my-iac-repo. No README or .gitignore. Copy the URL. +Go to your Github account and create a new repository called iac-repo. No README or .gitignore. Copy the URL. Clone locally: @@ -27,7 +27,7 @@ $ git clone Create a directory for this lab: ```bash -$ cd my-iac-repo +$ cd iac-repo $ mkdir 02-script ``` @@ -57,7 +57,7 @@ gcloud compute instances create node-svc \ --machine-type n1-standard-1 # add firewall rule -gcloud compute firewall-rules create allow-node-svc-3000 \ +gcloud compute firewall-rules create allow-node-svc-tcp-3000 \ --network default \ --action allow \ --direction ingress \ @@ -85,13 +85,13 @@ NAME NETWORK DIRECTION PRIORITY ALLOW DENY DISABLED allow-node-svc-3000 default INGRESS 1000 tcp:3000 False ``` -## Configuration script +## Deployment script -Before we can run our application, we need to create a running environment for it by installing dependent packages and configuring the OS. +Before we can run our application, we need to create a running environment for it by installing dependent packages and configuring the OS. Then we copy the application, initialize NPM and download express.js, and start the server. We are going to use the same commands we used before to do that, but this time, instead of running commands one by one, we'll create a `bash script` to save us some struggle. -In the `02-script` directory create a bash script `config.sh` to install node, npm, express, and git, and download the app and initialize node. +In the `02-script` directory create a bash script `deploy.sh` to install node, npm, express, and git, and download the app and initialize node. ```bash #!/bin/bash @@ -100,15 +100,6 @@ set -e # exit immediately if anything returns non-zero. See https://www.javatpo echo " ----- install node, npm, git ----- " apt-get update apt-get install -y nodejs npm git -``` - -## Deployment script - -Create a script for copying the application code from GitHub repository, initializing NPM and downloading express.js, and starting the server. Save it as `deploy.sh` inside `02-script` directory: - -```bash -#!/bin/bash -set -e echo " ----- download, initialize, and run app ----- " git clone https://github.com/dm-academy/node-svc-v1 @@ -122,17 +113,25 @@ nodejs server.js & ## Run the scripts -Copy the `02-script` directory to the created VM: +Copy the `deploy.sh` script to the created VM: ```bash $ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc) -$ scp -r . node-user@${INSTANCE_IP}:/home/node-user +$ scp -r deploy.sh node-user@${INSTANCE_IP}:/home/node-user ``` + +If sucessful, you should see something like: + +```bash +deploy.sh 100% 307 408.3KB/s 00:00 +``` + NOTE: If you get an `offending ECDSA key` error, use the suggested removal command. -NOTE: If you get the error `Permission denied (publickey).`, this probably means that your ssh-agent no longer has the raddit-user private key added. This easily happens if the Google Cloud Shell goes to sleep and wipes out your session. Check via issuing `ssh-add -l`. +NOTE: If you get the error `Permission denied (publickey).`, this probably means that your ssh-agent no longer has the node-user private key added. This easily happens if the Google Cloud Shell goes to sleep and wipes out your session. Check via issuing `ssh-add -l`. If you get a message to the effect that your agent is not running, type ``eval `ssh-agent` `` and then `ssh-add -l`. + You should see something like `2048 SHA256:bII5VsQY3fCWXEai0lUeChEYPaagMXun3nB9U2eoUEM /home/betz4871/.ssh/node-user (RSA)`. If you do not, re-issue the command `ssh-add ~/.ssh/node-user` and re-confirm with `ssh-add -l`. @@ -143,11 +142,19 @@ $ ssh node-user@${INSTANCE_IP} Run the scripts: ```bash -$ chmod +x ./config/*.sh -$ sudo ./config/configuration.sh -$ ./config/deploy.sh +$ chmod +x deploy.sh +$ sudo ./deploy.sh +``` + +To test that the server is running locally, type: +```bash +$ curl localhost:3000 ``` +You should receive this: +```bash +Successful request. +``` ## Access the Application Access the application in your browser by its public IP (don't forget to specify the port 3000). @@ -164,21 +171,20 @@ In the `provision` directory create a script `deprovision.sh`. ```bash #!/bin/bash -gcloud compute instances delete -q node-instance -gcloud compute firewall-rules delete -q allow-node-tcp-3000 +gcloud compute instances delete -q node-svc +gcloud compute firewall-rules delete -q allow-node-svc-tcp-3000 ``` Set permissions correctly (see previous) and execute. You should get results like: ```bash -Xxxxxx -``` +Deleted [https://www.googleapis.com/compute/v1/projects/proven-sum-252123/zones/us-central1-c/instances/node-svc]. +Deleted [https://www.googleapis.com/compute/v1/projects/proven-sum-252123/global/firewalls/allow-node-svc-tcp-3000].``` ## Save and commit the work Save and commit the scripts created in this lab into your `iac-tutorial` repo. - ## Conclusion Scripts helped us to save some time and effort of manually running every command one by one to configure the system and start the application. From 5934d1e50ae1bffc341649b50a6d71a1ece59528 Mon Sep 17 00:00:00 2001 From: Charles Betz Date: Sat, 15 Aug 2020 13:36:55 -0500 Subject: [PATCH 86/93] 04 updates --- docs/03-scripts.md | 33 +++++++++-------- docs/04-packer.md | 92 ++++++++++++++++++++++++---------------------- 2 files changed, 66 insertions(+), 59 deletions(-) diff --git a/docs/03-scripts.md b/docs/03-scripts.md index d19fff9..b2d9429 100644 --- a/docs/03-scripts.md +++ b/docs/03-scripts.md @@ -28,14 +28,15 @@ Create a directory for this lab: ```bash $ cd iac-repo -$ mkdir 02-script +$ mkdir 03-script +$ cd 03-script ``` To push your changes up to Github: ```bash $ git add . -A -$ git commit -m "first lab 02 commit" # should be relevant to the changes you made +$ git commit -m "first lab 03 commit" # should be relevant to the changes you made $ git push origin master ``` @@ -54,7 +55,7 @@ gcloud compute instances create node-svc \ --image-family ubuntu-1604-lts \ --image-project ubuntu-os-cloud \ --boot-disk-size 10GB \ - --machine-type n1-standard-1 + --machine-type f1-micro # add firewall rule gcloud compute firewall-rules create allow-node-svc-tcp-3000 \ @@ -85,13 +86,13 @@ NAME NETWORK DIRECTION PRIORITY ALLOW DENY DISABLED allow-node-svc-3000 default INGRESS 1000 tcp:3000 False ``` -## Deployment script +## Configuiration and deployment scripts Before we can run our application, we need to create a running environment for it by installing dependent packages and configuring the OS. Then we copy the application, initialize NPM and download express.js, and start the server. We are going to use the same commands we used before to do that, but this time, instead of running commands one by one, we'll create a `bash script` to save us some struggle. -In the `02-script` directory create a bash script `deploy.sh` to install node, npm, express, and git, and download the app and initialize node. +In the `03-script` directory create a bash script `install.sh` to install node, npm, express, and git, and download the app and initialize node. ```bash #!/bin/bash @@ -107,23 +108,23 @@ cd node-svc-v1 git checkout 02 npm install npm install express -nodejs server.js & - ``` + + ## Run the scripts -Copy the `deploy.sh` script to the created VM: +Copy the script to the created VM: ```bash $ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc) -$ scp -r deploy.sh node-user@${INSTANCE_IP}:/home/node-user +$ scp -r install.sh node-user@${INSTANCE_IP}:/home/node-user ``` If sucessful, you should see something like: ```bash -deploy.sh 100% 307 408.3KB/s 00:00 +install.sh 100% 214 279.9KB/s 00:00 ``` NOTE: If you get an `offending ECDSA key` error, use the suggested removal command. @@ -139,12 +140,15 @@ Connect to the VM via SSH: ```bash $ ssh node-user@${INSTANCE_IP} ``` +Have a look at what's in the directory (use `ls` and `cat`). Do you understand exactly how it got there? If you do not, ask. -Run the scripts: +Run the script and launch the server: ```bash -$ chmod +x deploy.sh -$ sudo ./deploy.sh +$ chmod +x install.sh +$ sudo ./install.sh +$ sudo nodejs node-svc-v1/server.js & ``` +The last output should be `Running on 3000`. You may need to hit Return or Enter to get a command prompt. To test that the server is running locally, type: ```bash @@ -162,9 +166,8 @@ Access the application in your browser by its public IP (don't forget to specify Open another terminal and run the following command to get a public IP of the VM: ```bash -$ gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-instance +$ gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc ``` - ## Destroy (de-provision) the resources by script In the `provision` directory create a script `deprovision.sh`. diff --git a/docs/04-packer.md b/docs/04-packer.md index 14ccdf5..86999b8 100644 --- a/docs/04-packer.md +++ b/docs/04-packer.md @@ -6,9 +6,9 @@ In this lab, we're going to take a look at the first IaC tool in this tutorial c ## Intro -Remember how in the second lab we had to make sure that the `git` was installed on the VM so that we could clone the application repo? Did it surprise you in a good way that the `git` was already installed on the system and we could skip the installation? +Remember how in the second lab we had to make install nodejs, npm, and even git on the VM so that we could clone the application repo? Did it surprise you that `git` was not already installed on the system? -Imagine how nice it would be to have other required packages like Ruby and Bundler preinstalled on the VM we provision, or have necessary configuration files come with the image, too. This would require even less time and effort from us to configure the system and run our application. +Imagine how nice it would be to have required packages like nodejs and npm preinstalled on the VM we provision, or have necessary configuration files come with the image, too. This would require even less time and effort from us to configure the system and run our application. Luckily, we can create custom machine images with required configuration and software installed using Packer. Let's check it out. @@ -26,7 +26,7 @@ $ packer -v ## Infrastructure as Code project -Create a new directory called `packer` inside your `iac-tutorial` repo, which we'll use to save the work done in this lab. +Create a new directory called `04-packer` inside your `iac-tutorial` repo, which we'll use to save the work done in this lab. ## Define image builder @@ -36,21 +36,21 @@ The part of packer responsible for starting a VM and creating an image from it i So before using packer to create images, we need to define a builder configuration in a JSON file (which is called **template** in Packer terminology). -Create a `raddit-base-image.json` file inside the `packer` directory with the following content (make sure to change the project ID and zone in case it's different): +Create a `node-svc-base-image.json` file inside the `packer` directory with the following content (make sure to change the project ID, and also the zone in case it's different): ```json { "builders": [ { "type": "googlecompute", - "project_id": "infrastructure-as-code", + "project_id": "YOUR PROJECT HERE. YOU MUST CHANGE THIS", "zone": "us-central1-c", - "machine_type": "g1-small", + "machine_type": "f1-micro", "source_image_family": "ubuntu-1604-lts", - "image_name": "raddit-base-{{isotime `20060102-150405`}}", - "image_family": "raddit-base", - "image_description": "Ubuntu 16.04 with Ruby, Bundler and MongoDB preinstalled", - "ssh_username": "raddit-user" + "image_name": "node-svc-base-{{isotime `20200901-000001`}}", + "image_family": "node-svc-base", + "image_description": "Ubuntu 16.04 with git, nodejs, npm preinstalled", + "ssh_username": "node-user" } ] } @@ -61,14 +61,14 @@ This template describes where and what type of a VM to launch for image creation Validate the template: ```bash -$ packer validate ./packer/raddit-base-image.json +$ packer validate node-svc-base-image.json ``` ## Define image provisioner As we already mentioned, builders are only responsible for starting a VM and creating an image from that VM. The real work of system configuration and installing software on the running VM is done by another Packer component called **provisioner**. -Add a [shell provisioner](https://www.packer.io/docs/provisioners/shell.html) to your template to run the `configuration.sh` script you created in the previous lab. +Add a [shell provisioner](https://www.packer.io/docs/provisioners/shell.html) to your template to run the `deploy.sh` script you created in the previous lab. Your template should look similar to this one: @@ -77,20 +77,20 @@ Your template should look similar to this one: "builders": [ { "type": "googlecompute", - "project_id": "infrastructure-as-code", + "project_id": "YOUR PROJECT HERE. YOU MUST CHANGE THIS", "zone": "us-central1-c", - "machine_type": "g1-small", + "machine_type": "f1-micro", "source_image_family": "ubuntu-1604-lts", - "image_name": "raddit-base-{{isotime `20060102-150405`}}", - "image_family": "raddit-base", - "image_description": "Ubuntu 16.04 with Ruby, Bundler and MongoDB preinstalled", - "ssh_username": "raddit-user" + "image_name": "node-svc-base-{{isotime `20200901-000001`}}", + "image_family": "node-svc-base", + "image_description": "Ubuntu 16.04 with git, nodejs, npm, and node-svc preinstalled", + "ssh_username": "node-user" } ], "provisioners": [ { "type": "shell", - "script": "{{template_dir}}/../scripts/configuration.sh", + "script": "{{template_dir}}/../03-script/install.sh", "execute_command": "sudo {{.Path}}" } ] @@ -108,7 +108,7 @@ $ packer validate ./packer/raddit-base-image.json Build the image for your application: ```bash -$ packer build ./packer/raddit-base-image.json +$ packer build node-svc-base-image.json ``` If you go to the [Compute Engine Images](https://console.cloud.google.com/compute/images) page you should see your new custom image. @@ -118,19 +118,19 @@ If you go to the [Compute Engine Images](https://console.cloud.google.com/comput Once the image is built, use it as a boot disk to start a VM: ```bash -$ gcloud compute instances create raddit-instance-4 \ - --image-family raddit-base \ +$ gcloud compute instances create node-svc \ + --image-family node-svc-base \ --boot-disk-size 10GB \ - --machine-type n1-standard-1 + --machine-type f1-micro ``` ## Deploy Application -Copy `deploy.sh` script to the created VM (be careful of the path in the second command, be sure of your context): +Connect to the VM via SSH: ```bash -$ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe raddit-instance-4) -$ scp ./scripts/config/deploy.sh raddit-user@${INSTANCE_IP}:/home/raddit-user +$ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc) +$ ssh node-user@${INSTANCE_IP} ``` NOTE: If you get an offending ECDSA key error, use the suggested removal command. @@ -140,22 +140,27 @@ NOTE: If you get the error `Permission denied (publickey).`, this probably means Connect to the VM via SSH: ```bash -$ ssh raddit-user@${INSTANCE_IP} +$ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc) +$ ssh node-user@${INSTANCE_IP} ``` -Verify Ruby, Bundler and MongoDB are installed: +Verify git, nodejs, npm, and the node-svc app are installed. Do you understand how they got there? (Your results may be slightly different, but if you get errors, investigate or ask for help): ```bash -$ ruby -v -$ bundle version -$ sudo systemctl status mongod +$ git --version +git version 2.7.4 +$ nodejs -v +v4.2.6 +$ npm -v +3.5.2 +$ ls node-svc-v1/ +LICENSE node_modules package.json README.md run.sh server.js test.sh ``` -Run deployment script: +Run server: ```bash -$ chmod +x ./deploy.sh -$ ./deploy.sh +$ sudo nodejs node-svc-v1/server.js & ``` ## Access Application @@ -163,26 +168,25 @@ $ ./deploy.sh Manually re-create the firewall rule: ```bash -$ gcloud compute firewall-rules create allow-raddit-tcp-9292 \ +$ gcloud compute firewall-rules create allow-node-svc-tcp-3000 \ --network default \ --action allow \ --direction ingress \ - --rules tcp:9292 \ + --rules tcp:3000 \ --source-ranges 0.0.0.0/0 ``` -Access the application in your browser by its public IP (don't forget to specify the port 9292). - Open another terminal and run the following command to get a public IP of the VM: ```bash -$ gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe raddit-instance-4 +$ gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc ``` +Access the application in your browser by its public IP (don't forget to specify the port 3000). + ## De-provision ```bash -$ gcloud compute instances delete -q raddit-instance-4 -$ gcloud compute firewall-rules delete -q allow-raddit-tcp-9292 +$ ../03-script/deprovision.sh #notice path ``` ## Save and commit the work @@ -191,13 +195,13 @@ Save and commit the packer template created in this lab into your `iac-tutorial` ## Learning more about Packer -Packer configuration files are called templates for a reason. They often get parameterized with [user variables](https://www.packer.io/docs/templates/user-variables.html). This could be very helpful since you can create multiple machine images with different configuration and for different purposes using one template file. +Packer configuration files are called templates for a reason. They often get parameterized with [user variables](https://www.packer.io/docs/templates/user-variables.html). This could be very helpful since you can create multiple machine images with different configurations for different purposes using one template file. Adding user variables to a template is easy, follow the [documentation](https://www.packer.io/docs/templates/user-variables.html) on how to do that. ## Immutable infrastructure -You may wonder why not to put everything inside the image including the application? Well, this approach is called an [immutable infrastructure](https://martinfowler.com/bliki/ImmutableServer.html). It is based on the idea `we build it once, and we never change it`. +By putting everything inside the image including the application, we have achieved an [immutable infrastructure](https://martinfowler.com/bliki/ImmutableServer.html). It is based on the idea `we build it once, and we never change it`. It has advantages of spending less time (zero in this case) on system configuration after VM's start, and prevents **configuration drift**, but it's also not easy to implement. @@ -205,7 +209,7 @@ It has advantages of spending less time (zero in this case) on system configurat In this lab you've used Packer to create a custom machine image for running your application. -The advantages of its usage are quite obvious: +Its advantages include: * `It requires less time and effort to configure a new VM for running the application` * `System configuration becomes more reliable.` When we start a new VM to deploy the application, we know for sure that it has the right packages installed and configured properly, since we built and tested the image. From 2f6c857248233fd60a64c6a85f38022c1e0599c3 Mon Sep 17 00:00:00 2001 From: Charles Betz Date: Sat, 15 Aug 2020 14:29:16 -0500 Subject: [PATCH 87/93] 05 working --- docs/05-terraform.md | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/docs/05-terraform.md b/docs/05-terraform.md index 9a8282a..7c90313 100644 --- a/docs/05-terraform.md +++ b/docs/05-terraform.md @@ -15,19 +15,19 @@ Remember, that each time we want to deploy an application, we have to `provision We do it via a `gcloud` command like this: ```bash -$ gcloud compute instances create raddit-instance-4 \ - --image-family raddit-base \ +$ gcloud compute instances create node-svc \ + --image-family node-svc-base \ --boot-disk-size 10GB \ - --machine-type n1-standard-1 + --machine-type f1-micro ``` -At this stage, it doesn't seem like there are any problems with this. But, in fact, there is. +At this stage, it doesn't seem like there are any problems with this. But, in fact, there are. -Infrastructure for running your services and applications could be huge. You might have tens, hundreds or even thousands of virtual machines, hundreds of firewall rules, multiples VPC networks, and load balancers. In addition to that, the infrastructure could be split between multiple teams and managed separately. Such infrastructure looks very complex and yet should be run and managed in a consistent and predictable way. +Infrastructure for running your services and applications could be huge. You might have tens, hundreds or even thousands of virtual machines, hundreds of firewall rules, multiple VPC networks and load balancers. Additionally, the infrastructure could be split between multiple teams. Such infrastructure looks, and is, very complex and yet should be run and managed in a consistent and predictable way. -If we create and change infrastructure components using gcloud CLI tool or Web UI Console, over time we won't be able to describe exactly in which `state` our infrastructure is in right now, meaning `we lose control over it`. +If we create and change infrastructure components using the Web User Interface (UI) Console or even the gcloud command ine interface (CLI) tool, over time we won't be able to describe exactly in which `state` our infrastructure is in right now, meaning `we lose control over it`. -This happens because you tend to forget what changes you've made a few months ago and why you did it. If multiple people are managing infrastructure, this makes things even worse, because you can't know what changes other people are making even though your communication inside the team could be great. +This happens because you tend to forget what changes you've made a few months ago and why you made them. If multiple people across multiple teams are managing infrastructure, this makes things even worse. So we see here 2 clear problems: @@ -50,28 +50,28 @@ $ terraform -v ## Infrastructure as Code project -Create a new directory called `terraform` inside your `iac-tutorial` repo, which we'll use to save the work done in this lab. +Create a new directory called `05-terraform` inside your `iac-tutorial` repo, which we'll use to save the work done in this lab. ## Describe VM instance _Terraform allows you to describe the desired state of your infrastructure and makes sure your desired state meets the actual state._ -Terraform uses [**resources**](https://www.terraform.io/docs/configuration/resources.html) to describe different infrastructure components. If you want to use Terraform to manage some infrastructure component, you should first make sure there is a resource for that component for that particular platform. +Terraform uses [**resources**](https://www.terraform.io/docs/configuration/resources.html) to describe different infrastructure components. If you want to use Terraform to manage an infrastructure component, you should first make sure there is a resource for that component for that particular platform. Let's use Terraform syntax to describe a VM instance that we want to be running. -Create a Terraform configuration file called `main.tf` inside the `terraform` directory with the following content: +Create a Terraform configuration file called `main.tf` inside the `05-terraform` directory with the following content: ``` -resource "google_compute_instance" "raddit" { - name = "raddit-instance" - machine_type = "n1-standard-1" +resource "google_compute_instance" "node-svc" { + name = "node-svc" + machine_type = "f1-micro" zone = "us-central1-c" # boot disk specifications boot_disk { initialize_params { - image = "raddit-base" // use image built with Packer + image = "node-svc-base" // use image built with Packer } } @@ -98,13 +98,11 @@ Create another file inside `terraform` folder and call it `providers.tf`. Put pr ``` provider "google" { version = "~> 2.5.0" - project = "infrastructure-as-code" + project = "YOU MUST PUT YOUR PROJECT NAME HERE" region = "us-central1-c" } ``` -Note the `region` value, this is where terraform will provision resources (you may wish to change it). - Make sure to change the `project` value in provider's configuration above to your project's ID. You can get your default project's ID by running the command: ```bash @@ -114,7 +112,6 @@ $ gcloud config list project Now run the `init` command inside `terraform` directory to download the provider: ```bash -$ cd ./terraform $ terraform init ``` From c7f218928ece401b9523e984db99733fbc9265c3 Mon Sep 17 00:00:00 2001 From: Charles Betz Date: Sat, 15 Aug 2020 17:46:17 -0500 Subject: [PATCH 88/93] 05 terraform --- docs/05-terraform.md | 98 +++++++++++++++++++++++++------------------- 1 file changed, 56 insertions(+), 42 deletions(-) diff --git a/docs/05-terraform.md b/docs/05-terraform.md index 7c90313..44567d0 100644 --- a/docs/05-terraform.md +++ b/docs/05-terraform.md @@ -126,58 +126,61 @@ $ terraform apply After Terraform ran successfully, use a gcloud command to verify that the machine was indeed launched: ```bash -$ gcloud compute instances describe raddit-instance +$ gcloud compute instances describe node-svc ``` ## Deploy Application -We did provisioning via Terraform, but we still need to run a script to deploy our application. +We did provisioning via Terraform, but we still need to run a command to start our application. Let's do this remotely this time, instead of logging into the machine: -Copy `deploy.sh` script to the created VM: +Get the IP of the created VM: ```bash -$ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe raddit-instance) -$ scp ../scripts/deploy.sh raddit-user@${INSTANCE_IP}:/home/raddit-user +$ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc) +$ rsh ${INSTANCE_IP} -l node-user sudo nodejs /home/node-user/node-svc-v1/server.js & ``` NOTE: If you get an offending ECDSA key error, use the suggested removal command. -NOTE: If you get the error `Permission denied (publickey).`, this probably means that your ssh-agent no longer has the raddit-user private key added. This easily happens if the Google Cloud Shell goes to sleep and wipes out your session. Check via issuing `ssh-add -l`. You should see something like `2048 SHA256:bII5VsQY3fCWXEai0lUeChEYPaagMXun3nB9U2eoUEM /home/betz4871/.ssh/raddit-user (RSA)`. If you do not, re-issue the command `ssh-add ~/.ssh/raddit-user` and re-confirm with `ssh-add -l`. +NOTE: If you get the error `Permission denied (publickey).`, this probably means that your ssh-agent no longer has the raddit-user private key added. This easily happens if the Google Cloud Shell goes to sleep and wipes out your session. Check via issuing `ssh-add -l`. You should see something like `2048 SHA256:bII5VsQY3fCWXEai0lUeChEYPaagMXun3nB9U2eoUEM /home/betz4871/.ssh/raddit-user (RSA)`. If you do not, re-issue the command `ssh-add ~/.ssh/node-user` and re-confirm with `ssh-add -l`. Connect to the VM via SSH: ```bash -$ ssh raddit-user@${INSTANCE_IP} +$ ssh node-user@${INSTANCE_IP} ``` -Run deployment script: +Check that servce is running, and then exit: ```bash -$ chmod +x ./deploy.sh -$ ./deploy.sh +node-user@node-svc:~$ curl localhost:3000 +Successful request. +node-user@node-svc:~$ exit ``` -## Access the Application +## Access the Application Externally7 Manually create the firewall rule: ```bash -$ gcloud compute firewall-rules create allow-raddit-tcp-9292 \ +$ gcloud compute firewall-rules create allow-node-svc-tcp-3000 \ --network default \ --action allow \ --direction ingress \ - --rules tcp:9292 \ + --rules tcp:3000 \ --source-ranges 0.0.0.0/0 ``` -Access the application in your browser by its public IP (don't forget to specify the port 9292). Open another terminal and run the following command to get a public IP of the VM: ```bash -$ gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe raddit-instance +$ gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc ``` +Access the application in your browser by its public IP (don't forget to specify the port 3000). + + ## Add other GCP resources into Terraform Let's add ssh keys and the firewall rule into our Terraform configuration so that we know for sure those resources are present. @@ -186,24 +189,24 @@ First, delete the SSH project key and firewall rule: ```bash $ gcloud compute project-info remove-metadata --keys=ssh-keys -$ gcloud compute firewall-rules delete allow-raddit-tcp-9292 +$ gcloud compute firewall-rules delete allow-node-svc-tcp-3000 ``` -Make sure that your application became inaccessible via port 9292 and SSH connection with a private key of `raddit-user` fails. +Make sure that your application became inaccessible via port 3000 and SSH connection with a private key of `node-user` fails. Then add appropriate resources into `main.tf` file. Your final version of `main.tf` file should look similar to this (change the ssh key file path, if necessary): -``` -resource "google_compute_instance" "raddit" { - name = "raddit-instance" - machine_type = "n1-standard-1" +```bash +resource "google_compute_instance" "node-svc" { + name = "node-svc" + machine_type = "f1-micro" zone = "us-central1-c" # boot disk specifications boot_disk { initialize_params { - image = "raddit-base" // use image built with Packer + image = "node-svc-base" // use image built with Packer } } @@ -214,18 +217,18 @@ resource "google_compute_instance" "raddit" { } } -resource "google_compute_project_metadata" "raddit" { +resource "google_compute_project_metadata" "node-svc" { metadata = { - ssh-keys = "raddit-user:${file("~/.ssh/raddit-user.pub")}" // path to ssh key file + ssh-keys = "node-user:${file("~/.ssh/node-user.pub")}" // path to ssh key file } } -resource "google_compute_firewall" "raddit" { - name = "allow-raddit-tcp-9292" +resource "google_compute_firewall" "node-svc" { + name = "allow-node-svc-tcp-3000" network = "default" allow { protocol = "tcp" - ports = ["9292"] + ports = ["3000"] } source_ranges = ["0.0.0.0/0"] } @@ -237,54 +240,65 @@ Tell Terraform to apply the changes to bring the actual infrastructure state to $ terraform apply ``` -Verify that the application became accessible again on port 9292 and SSH connection with a private key works. +Using the same techniques as above, verify that the application became accessible again on port 3000 (locally and remotely) and SSH connection with a private key works. Here's a new way to check it from the Google Cloud Shell (you don't ssh into the VM): + +```bash +$ curl $INSTANCE_IP:3000 +``` ## Create an output variable -Remember how we often had to use a gcloud command like this to retrive a public IP address of a VM? +We have frequntly used this gcloud command to retrieve a public IP address of a VM: ```bash -$ gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe raddit-instance +$ gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc ``` We can tell Terraform to provide us this information using [output variables](https://www.terraform.io/intro/getting-started/outputs.html). Create another configuration file inside `terraform` directory and call it `outputs.tf`. Put the following content in it: -``` -output "raddit_public_ip" { - value = "${google_compute_instance.raddit.network_interface.0.access_config.0.nat_ip}" +```json +output "node_svc_public_ip" { + value = "${google_compute_instance.node-svc.network_interface.0.access_config.0.nat_ip}" } ``` -Run terraform apply again: +Run terraform apply again, this time with auto approve: ```bash -$ terraform apply +$ terraform apply -auto-approve + +google_compute_instance.node-svc: Refreshing state... [id=node-svc] +google_compute_firewall.node-svc: Refreshing state... [id=allow-node-svc-tcp-3000] +google_compute_project_metadata.node-svc: Refreshing state... [id=proven-sum-252123] +Apply complete! Resources: 0 added, 0 changed, 0 destroyed. +Outputs: +node_svc_public_ip = 34.71.90.74 ``` -You should see the public IP of the VM we created. +Couple of things to notice here. First, we did not destroy anything, so terraform refreshes - it confirms that configurations are still as specified. During this Terraform run, no resources have been created or changed, which means that the actual state of our infrastructure already meets the requirements of a desired state. -Also note, that during this Terraform run, no resources have been created or changed, which means that the actual state of our infrastructure already meets the requirements of a desired state. +Secondly, under "Outputs:", you should see the public IP of the VM we created. ## Save and commit the work -Save and commit the `terraform` folder created in this lab into your `iac-tutorial` repo. +Save and commit the `05-terraform` folder created in this lab into your `iac-tutorial` repo. ## Conclusion -In this lab, you saw in its most obvious way the application of Infrastructure as Code practice. +In this lab, you saw a state of the art the application of Infrastructure as Code practice. -We used `code` (Terraform configuration syntax) to describe the `desired state` of the infrastructure. Then we told Terraform to bring the actual state of the infrastructure to the desired state we described. +We used *code* (Terraform configuration syntax) to describe the *desired state* of the infrastructure. Then we told Terraform to bring the actual state of the infrastructure to the desired state we described. -With this approach, Terraform configuration becomes `a single source of truth` about the current state of your infrastructure. Moreover, the infrastructure is described as code, so we can apply to it the same practices we commonly use in development such as keeping the code in source control, use peer reviews for making changes, etc. +With this approach, Terraform configuration becomes *a single source of truth* about the current state of your infrastructure. Moreover, the infrastructure is described as code, so we can apply to it the same practices we commonly use in development such as keeping the code in source control, use peer reviews for making changes, etc. All of this helps us get control over even the most complex infrastructure. Destroy the resources created by Terraform and move on to the next lab. ```bash -$ terraform destroy +$ terraform destroy -auto-approve ``` Next: [Ansible](06-ansible.md) From 4f1c89d2418bdba96b8e81eae012cf06fce2a662 Mon Sep 17 00:00:00 2001 From: Charles Betz Date: Sat, 15 Aug 2020 20:36:07 -0500 Subject: [PATCH 89/93] 06 --- docs/03-scripts.md | 17 +++-- docs/04-packer.md | 20 +++--- docs/05-terraform.md | 12 ++-- docs/06-ansible.md | 163 +++++++++++++++++++++++++------------------ 4 files changed, 124 insertions(+), 88 deletions(-) diff --git a/docs/03-scripts.md b/docs/03-scripts.md index b2d9429..52d80c2 100644 --- a/docs/03-scripts.md +++ b/docs/03-scripts.md @@ -86,13 +86,13 @@ NAME NETWORK DIRECTION PRIORITY ALLOW DENY DISABLED allow-node-svc-3000 default INGRESS 1000 tcp:3000 False ``` -## Configuiration and deployment scripts +## Installation script Before we can run our application, we need to create a running environment for it by installing dependent packages and configuring the OS. Then we copy the application, initialize NPM and download express.js, and start the server. We are going to use the same commands we used before to do that, but this time, instead of running commands one by one, we'll create a `bash script` to save us some struggle. -In the `03-script` directory create a bash script `install.sh` to install node, npm, express, and git, and download the app and initialize node. +In the `03-script` directory create bash script `config.sh` to install node, npm, express, and git. Create a script `install.sh` to download the app and initialize node. ```bash #!/bin/bash @@ -101,6 +101,11 @@ set -e # exit immediately if anything returns non-zero. See https://www.javatpo echo " ----- install node, npm, git ----- " apt-get update apt-get install -y nodejs npm git +``` + +```bash +#!/bin/bash +set -e # exit immediately if anything returns non-zero. See https://www.javatpoint.com/linux-set-command echo " ----- download, initialize, and run app ----- " git clone https://github.com/dm-academy/node-svc-v1 @@ -110,6 +115,7 @@ npm install npm install express ``` +NOTE: Why two scripts? Discuss in class. ## Run the scripts @@ -118,12 +124,13 @@ Copy the script to the created VM: ```bash $ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc) -$ scp -r install.sh node-user@${INSTANCE_IP}:/home/node-user +$ scp -r config.sh install.sh node-user@${INSTANCE_IP}:/home/node-user ``` If sucessful, you should see something like: ```bash +config.sh 100% 214 279.9KB/s 00:00 install.sh 100% 214 279.9KB/s 00:00 ``` @@ -144,8 +151,8 @@ Have a look at what's in the directory (use `ls` and `cat`). Do you understand e Run the script and launch the server: ```bash -$ chmod +x install.sh -$ sudo ./install.sh +$ chmod +x *install*.sh +$ sudo ./config.sh && ./install.sh # running 2 commands on one line $ sudo nodejs node-svc-v1/server.js & ``` The last output should be `Running on 3000`. You may need to hit Return or Enter to get a command prompt. diff --git a/docs/04-packer.md b/docs/04-packer.md index 86999b8..cb778c0 100644 --- a/docs/04-packer.md +++ b/docs/04-packer.md @@ -90,7 +90,7 @@ Your template should look similar to this one: "provisioners": [ { "type": "shell", - "script": "{{template_dir}}/../03-script/install.sh", + "script": "{{template_dir}}/../03-script/config.sh", "execute_command": "sudo {{.Path}}" } ] @@ -100,7 +100,7 @@ Your template should look similar to this one: Make sure the template is valid: ```bash -$ packer validate ./packer/raddit-base-image.json +$ packer validate ./packer/node-base-image.json ``` ## Create custom machine image @@ -126,16 +126,10 @@ $ gcloud compute instances create node-svc \ ## Deploy Application -Connect to the VM via SSH: +Copy the installation script to the VM: -```bash $ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc) -$ ssh node-user@${INSTANCE_IP} -``` - -NOTE: If you get an offending ECDSA key error, use the suggested removal command. - -NOTE: If you get the error `Permission denied (publickey).`, this probably means that your ssh-agent no longer has the raddit-user private key added. This easily happens if the Google Cloud Shell goes to sleep and wipes out your session. Check via issuing `ssh-add -l`. You should see something like `2048 SHA256:bII5VsQY3fCWXEai0lUeChEYPaagMXun3nB9U2eoUEM /home/betz4871/.ssh/raddit-user (RSA)`. If you do not, re-issue the command `ssh-add ~/.ssh/raddit-user` and re-confirm with `ssh-add -l`. +$ scp -r install.sh node-user@${INSTANCE_IP}:/home/node-user Connect to the VM via SSH: @@ -144,6 +138,10 @@ $ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].nat $ ssh node-user@${INSTANCE_IP} ``` +NOTE: If you get an offending ECDSA key error, use the suggested removal command. + +NOTE: If you get the error `Permission denied (publickey).`, this probably means that your ssh-agent no longer has the node-user private key added. This easily happens if the Google Cloud Shell goes to sleep and wipes out your session. Check via issuing `ssh-add -l`. You should see something like `2048 SHA256:bII5VsQY3fCWXEai0lUeChEYPaagMXun3nB9U2eoUEM /home/betz4871/.ssh/node-user (RSA)`. If you do not, re-issue the command `ssh-add ~/.ssh/node-user` and re-confirm with `ssh-add -l`. + Verify git, nodejs, npm, and the node-svc app are installed. Do you understand how they got there? (Your results may be slightly different, but if you get errors, investigate or ask for help): ```bash @@ -157,6 +155,8 @@ $ ls node-svc-v1/ LICENSE node_modules package.json README.md run.sh server.js test.sh ``` + + Run server: ```bash diff --git a/docs/05-terraform.md b/docs/05-terraform.md index 44567d0..acbafe1 100644 --- a/docs/05-terraform.md +++ b/docs/05-terraform.md @@ -131,18 +131,20 @@ $ gcloud compute instances describe node-svc ## Deploy Application -We did provisioning via Terraform, but we still need to run a command to start our application. Let's do this remotely this time, instead of logging into the machine: +We did provisioning via Terraform, but we still need to install and start our application. Let's do this remotely this time, instead of logging into the machine: -Get the IP of the created VM: ```bash -$ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc) -$ rsh ${INSTANCE_IP} -l node-user sudo nodejs /home/node-user/node-svc-v1/server.js & +$ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc) # get IP of VM +$ scp -r install.sh node-user@${INSTANCE_IP}:/home/node-user # copy install script +$ rsh ${INSTANCE_IP} -l node-user chmod +x /home/node-user/install.sh # set permissions +$ rsh ${INSTANCE_IP} -l node-user /home/node-user/install.sh # install app +$ rsh ${INSTANCE_IP} -l node-user sudo nodejs /home/node-user/node-svc-v1/server.js & # run app ``` NOTE: If you get an offending ECDSA key error, use the suggested removal command. -NOTE: If you get the error `Permission denied (publickey).`, this probably means that your ssh-agent no longer has the raddit-user private key added. This easily happens if the Google Cloud Shell goes to sleep and wipes out your session. Check via issuing `ssh-add -l`. You should see something like `2048 SHA256:bII5VsQY3fCWXEai0lUeChEYPaagMXun3nB9U2eoUEM /home/betz4871/.ssh/raddit-user (RSA)`. If you do not, re-issue the command `ssh-add ~/.ssh/node-user` and re-confirm with `ssh-add -l`. +NOTE: If you get the error `Permission denied (publickey).`, this probably means that your ssh-agent no longer has the node-user private key added. This easily happens if the Google Cloud Shell goes to sleep and wipes out your session. Check via issuing `ssh-add -l`. You should see something like `2048 SHA256:bII5VsQY3fCWXEai0lUeChEYPaagMXun3nB9U2eoUEM /home/betz4871/.ssh/node-user (RSA)`. If you do not, re-issue the command `ssh-add ~/.ssh/node-user` and re-confirm with `ssh-add -l`. Connect to the VM via SSH: diff --git a/docs/06-ansible.md b/docs/06-ansible.md index 5bce19f..f7f9da9 100644 --- a/docs/06-ansible.md +++ b/docs/06-ansible.md @@ -1,6 +1,6 @@ # Ansible -In the previous lab, you used Terraform to implement Infrastructure as Code approach to managing the cloud infrastructure resources. Yet, we have another type of tooling to discover and that is **Configuration Management** (CM) tools. +In the previous lab, you used Terraform to implement Infrastructure as Code approach to managing the cloud infrastructure resources. There is another major type of tooling we need to consider, and that is **Configuration Management** (CM) tools. When talking about CM tools, we can often meet the acronym `CAPS` which stands for Chef, Ansible, Puppet and Saltstack - the most known and commonly used CM tools. In this lab, we're going to look at Ansible and see how CM tools can help us improve our operations. @@ -14,7 +14,7 @@ _Scripts are bad at long term management of system configuration, because they m When you write a script, you use a scripting language syntax (Bash, Python) to write commands which you think should change the system's configuration. And the problem is that there are too many ways people can write the code that is meant to do the same things, which is the reason why scripts are often difficult to read and understand. Besides, there are various choices as to what language to use for a script: should you write it in Ruby which your colleagues know very well or Bash which you know better? -Common configuration management operations are well-known: copy a file to a remote machine, create a folder, start/stop/enable a process, install packages, etc. So _we need a tool that would implement these common operations in a well-known tested way and provide us with a clean and understandable syntax for using them_. This way we wouldn't have to write complex scripts ourselves each time for the same tasks, possibly making mistakes along the way, but instead just tell the tool what should be done: what packages should be present, what processes should be started, etc. +Common configuration management operations are well-known: copy a file to a remote machine, create a folder, start/stop/enable a process, install packages, etc. So _we need a tool that would implement these common operations in a well-known and tested way, providing us with a clean and understandable syntax for using them_. This way we wouldn't have to write complex scripts ourselves each time for the same tasks, possibly making mistakes along the way, but instead just tell the tool what should be done: what packages should be present, what processes should be started, etc. This is exactly what CM tools do. So let's check it out using Ansible as an example. @@ -22,13 +22,13 @@ This is exactly what CM tools do. So let's check it out using Ansible as an exam NOTE: this lab assumes Ansible v2.4 is installed. It may not work as expected with other versions as things change quickly. -Issue the following commands: +Issue the following commands in the Google cloud shell (note that Ansible will not remain installed when your shell goes to sleep): -``` +```bash $ sudo apt update $ sudo apt install software-properties-common $ sudo apt-add-repository --yes --update ppa:ansible/ansible -$ sudo apt install ansible +$ sudo apt install -y ansible ``` If you have issues, reference the instructions on how to install Ansible on your system from [official documentation](http://docs.ansible.com/ansible/latest/intro_installation.html). @@ -41,15 +41,15 @@ $ ansible --version ## Infrastructure as Code project -Create a new directory called `ansible` inside your `iac-tutorial` repo, which we'll use to save the work done in this lab. +Create a new directory called `06-ansible` inside your `iac-tutorial` repo, which we'll use to save the work done in this lab. ## Provision compute resources Start a VM and create other GCP resources for running your application applying Terraform configuration you wrote in the previous lab: ```bash -$ cd ./terraform -$ terraform apply +$ cd ./05-terraform # adapt this command as necessary to get to the directory +$ terraform apply -auto-approve ``` ## Deploy playbook @@ -60,90 +60,117 @@ Ansible uses **tasks** to define commands used for system configuration. Each An Each task uses some **module** to perform a certain operation on the configured system. Modules are well tested functions which are meant to perform common system configuration operations. -Let's look at our `deploy.sh` script first to see what modules we might need to use: +Let's look at our `install.sh` first to see what modules we might need to use: ```bash #!/bin/bash -set -e - -echo " ----- clone application repository ----- " -git clone https://github.com/Artemmkin/raddit.git +set -e # exit immediately if anything returns non-zero. See https://www.javatpoint.com/linux-set-command -echo " ----- install dependent gems ----- " -cd ./raddit -sudo bundle install -echo " ----- start the application ----- " -sudo systemctl start raddit -sudo systemctl enable raddit +echo " ----- download, initialize, and run app ----- " +git clone https://github.com/dm-academy/node-svc-v1 +cd node-svc-v1 +git checkout 02 +npm install +npm install express ``` -We clearly see here 3 different types of operations: cloning a git repo, installing gems via Bundler, and managing a service via systemd. +We clearly see here several types of operations: cloning a git repo and setting the branch, initializing npm, and installing express (a Node package). + +We also, to start the service, need to run this command: + +`$ sudo nodejs /home/node-user/node-svc-v1/server.js &` So we'll search for Ansible modules that allow to perform these operations. Luckily, there are modules for all of these operations. -Ansible uses YAML syntax to define tasks, which makes the configuration looks clean. +Ansible uses YAML syntax to define tasks, which makes the configuration readable. -Let's create a file called `deploy.yml` inside the `ansible` directory: +Let's create a file called `deploy.yml` ("deploy" including both installation and launching) inside the `ansible` directory: ```yaml --- -- name: Deploy Raddit App - hosts: raddit-app +- name: Deploy node-svc App + hosts: node-svc tasks: - name: Fetch the latest version of application code + # see https://docs.ansible.com/ansible/latest/modules/git_module.html git: - repo: 'https://github.com/Artemmkin/raddit.git' - dest: /home/raddit-user/raddit - register: clone - - - name: Install application dependencies - become: true - bundler: - state: present - chdir: /home/raddit-user/raddit - when: clone.changed - notify: restart raddit - - handlers: - - name: restart raddit - become: true - systemd: name=raddit state=restarted + repo: 'https://github.com/dm-academy/node-svc-v1' + dest: /home/node-user/node-svc-1 + version: "02" + register: clone + + - name: NPM install express and initialize app + # see https://docs.ansible.com/ansible/latest/modules/npm_module.html + npm: + name: express + global: yes + + - name: Install packages based on package.json. + npm: + path: /home/node-user/node-svc-1 + + - name: Start the nodejs server + # see https://codelike.pro/deploy-nodejs-app-with-ansible-git-pm2/ + sudo_user: node-user + command: pm2 start server.js --name node-app chdir=/home/node-user/node-svc-1s + ignore_errors: yes + when: npm_finished.changed + ``` -In this configuration file, which is called a **playbook** in Ansible terminology, we define 3 tasks: +In this configuration file, which is called a **playbook** in Ansible terminology, we define several tasks. + +The `name` that precedes each task is used as a comment that will show up in the terminal when the task starts to run. + +`register` option allows to capture the result output from running a task. The `first task` uses git module to pull the code from GitHub. ```yaml - name: Fetch the latest version of application code - git: - repo: 'https://github.com/Artemmkin/raddit.git' - dest: /home/raddit-user/raddit - register: clone + # see https://docs.ansible.com/ansible/latest/modules/git_module.html + git: + repo: 'https://github.com/dm-academy/node-svc-v1' + dest: /home/node-user/node-svc-1 + version: 02 + register: git_finished ``` -The `name` that precedes each task is used as a comment that will show up in the terminal when the task starts to run. -`register` option allows to capture the result output from running a task. We will use it later in a conditional statement for running a `bundle install` task. +The second task installs the npm package express and initializes the app in the specified directory: + +```yaml + + - name: NPM install express and initialize app + # see https://docs.ansible.com/ansible/latest/modules/npm_module.html + npm: + name: coffee-script + global: yes -The second task runs bundler in the specified directory: + - name: Install packages based on package.json. + npm: + path: /home/node-user/node-svc-1 + +``` + +The third task runs the server: ```yaml -- name: Install application dependencies - become: true - bundler: - state: present - chdir: /home/raddit-user/raddit - when: clone.changed - notify: restart raddit + - name: Start the nodejs server + # see https://codelike.pro/deploy-nodejs-app-with-ansible-git-pm2/ + sudo_user: node-user + command: pm2 start server.js --name node-app chdir=/home/node-user/node-svc-1s + ignore_errors: yes + when: npm_finished.changed + ``` -Note, how for each module we use a different set of module options (in this case `state` and `chdir`). You can find full information about the options in a module's documentation. +Note, how for each module we use a different set of module options. You can find full information about the options in a module's documentation. -In the second task, we use a conditional statement [when](http://docs.ansible.com/ansible/latest/playbooks_conditionals.html#the-when-statement) to make sure the `bundle install` task is only run when the local repo was updated, i.e. the output from running git clone command was changed. This allows us to save some time spent on system configuration by not running unnecessary commands. +In the second task, we use a conditional statement [when](http://docs.ansible.com/ansible/latest/playbooks_conditionals.html#the-when-statement) to make sure the `npm install` task is only run when the local repo was updated, i.e. the output from running git clone command was changed. This allows us to save some time spent on system configuration by not running unnecessary commands. -On the same level as tasks, we also define a **handlers** block. Handlers are special tasks which are run only in response to notification events from other tasks. In our case, `raddit` service gets restarted only when the `bundle install` task is run. +On the same level as tasks, we also define a **handlers** block. Handlers are special tasks which are run only in response to notification events from other tasks. In our case, `node-svc` service gets restarted only when the `npm install` task is run. ## Inventory file @@ -154,21 +181,21 @@ To be able to connect to a remote VM, Ansible needs information like IP address Create a file called `hosts.yml` inside `ansible` directory with the following content (make sure to change the `ansible_host` parameter to public IP of your VM): ```yaml -raddit-app: +node-svc: hosts: - raddit-instance: + node-svc-01: ansible_host: 35.35.35.35 - ansible_user: raddit-user + ansible_user: node-user ``` -Here we define a group of hosts (`raddit-app`) under which we list the hosts that belong to this group. In this case, we list only one host under the hosts group and give it a name (`raddit-instance`) and information on how to connect to the host. +Here we define a group of hosts (`node-svc`) under which we list the hosts that belong to this group. In this case, we list only one host under the hosts group and give it a name (`node-svc-01`) and information on how to connect to the host. -Now note, that inside our `deploy.yml` playbook we specified `raddit-app` host group in the `hosts` option before the tasks: +Now note, that inside our `deploy.yml` playbook we specified `node-svc` host group in the `hosts` option before the tasks: ```yaml --- -- name: Deploy Raddit App - hosts: raddit-app +- name: Deploy node-svc app + hosts: node-svc-01 tasks: ... ``` @@ -184,7 +211,7 @@ Let's define custom Ansible configuration for our directory. Create a file calle ```ini [defaults] inventory = ./hosts.yml -private_key_file = ~/.ssh/raddit-user +private_key_file = ~/.ssh/node-user host_key_checking = False ``` @@ -197,13 +224,13 @@ Now it's time to run your playbook and see how it works. Use the following commands to start a deployment: ```bash -$ cd ./ansible +$ cd ./06-ansible $ ansible-playbook deploy.yml ``` ## Access Application -Access the application in your browser by its public IP (don't forget to specify the port 9292) and make sure application has been deployed and is functional. +Access the application in your browser by its public IP (don't forget to specify the port 3000) and make sure application has been deployed and is functional. ## Futher Learning Ansible From ddbd5d30aa62997cffc404dbc849bd86a801f833 Mon Sep 17 00:00:00 2001 From: Charles Betz Date: Sun, 16 Aug 2020 14:08:43 -0500 Subject: [PATCH 90/93] many changes --- docs/02-manual-operations.md | 12 +++--- docs/03-scripts.md | 4 +- docs/04-packer.md | 32 +++++++-------- docs/05-terraform.md | 8 ++-- docs/06-ansible.md | 4 +- docs/08-docker.md | 78 +++++++++++++++++------------------- 6 files changed, 66 insertions(+), 72 deletions(-) diff --git a/docs/02-manual-operations.md b/docs/02-manual-operations.md index 0221245..62c80d4 100644 --- a/docs/02-manual-operations.md +++ b/docs/02-manual-operations.md @@ -20,7 +20,7 @@ Use the following gcloud command in your terminal to launch a VM with Ubuntu 16. ```bash $ gcloud compute instances create node-svc\ - --image-family ubuntu-1604-lts \ + --image-family ubuntu-minimal-2004-lts \ --image-project ubuntu-os-cloud \ --boot-disk-size 10GB \ --machine-type f1-micro @@ -78,19 +78,19 @@ $ ssh node-user@${INSTANCE_IP} Install Node and npm: ```bash -$ sudo apt-get update -$ sudo apt-get install nodejs npm +$ +$ sudo apt-get install -y nodejs npm ``` Check the installed version of Node: ```bash -$ nodejs -v +$ node -v ``` Install `git`: ```bash -$ sudo apt install git +$ sudo apt -y install git ``` Clone the application repo into the home directory of `node-user` user (reminder, how do you clone to the right location?): @@ -98,7 +98,7 @@ Clone the application repo into the home directory of `node-user` user (reminder ```bash $ git clone https://github.com/dm-academy/node-svc-v1 ``` -Navigate to the repo and check out the 02 branch (matching this lesson) +Navigate to the repo (`cd node-svc-v1`) and check out the 02 branch (matching this lesson) ```bash $ git checkout 02 diff --git a/docs/03-scripts.md b/docs/03-scripts.md index 52d80c2..89950e3 100644 --- a/docs/03-scripts.md +++ b/docs/03-scripts.md @@ -52,7 +52,7 @@ In the `script` directory create a script `provision.sh`: #!/bin/bash # add new VM gcloud compute instances create node-svc \ - --image-family ubuntu-1604-lts \ + --image-family ubuntu-minimal-2004-lts \ --image-project ubuntu-os-cloud \ --boot-disk-size 10GB \ --machine-type f1-micro @@ -151,7 +151,7 @@ Have a look at what's in the directory (use `ls` and `cat`). Do you understand e Run the script and launch the server: ```bash -$ chmod +x *install*.sh +$ chmod +x *.sh $ sudo ./config.sh && ./install.sh # running 2 commands on one line $ sudo nodejs node-svc-v1/server.js & ``` diff --git a/docs/04-packer.md b/docs/04-packer.md index cb778c0..f926376 100644 --- a/docs/04-packer.md +++ b/docs/04-packer.md @@ -10,11 +10,11 @@ Remember how in the second lab we had to make install nodejs, npm, and even git Imagine how nice it would be to have required packages like nodejs and npm preinstalled on the VM we provision, or have necessary configuration files come with the image, too. This would require even less time and effort from us to configure the system and run our application. -Luckily, we can create custom machine images with required configuration and software installed using Packer. Let's check it out. +Luckily, we can create custom machine images with required configuration and software installed using Packer, an IaC tool by Hashicorp. Let's check it out. ## Install Packer -[Download](https://www.packer.io/downloads.html) and install Packer onto your system (this means the Google Cloud Shell). +[Download](https://www.packer.io/downloads.html) and install Packer onto your system (this means the Google Cloud Shell). You will need to figure this out. If you have issues, consult [this script](https://github.com/dm-academy/iac-tutorial-rsrc/blob/master/packer/install-packer.sh). @@ -46,8 +46,8 @@ Create a `node-svc-base-image.json` file inside the `packer` directory with the "project_id": "YOUR PROJECT HERE. YOU MUST CHANGE THIS", "zone": "us-central1-c", "machine_type": "f1-micro", - "source_image_family": "ubuntu-1604-lts", - "image_name": "node-svc-base-{{isotime `20200901-000001`}}", + "source_image_family": "ubuntu-minimal-2004-lts", + "image_name": "node-svc-base-{{isotime \"2006-01-02 03:04:05\"}}", "image_family": "node-svc-base", "image_description": "Ubuntu 16.04 with git, nodejs, npm preinstalled", "ssh_username": "node-user" @@ -129,12 +129,11 @@ $ gcloud compute instances create node-svc \ Copy the installation script to the VM: $ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc) -$ scp -r install.sh node-user@${INSTANCE_IP}:/home/node-user +$ scp -r ../03-script/install.sh node-user@${INSTANCE_IP}:/home/node-user Connect to the VM via SSH: ```bash -$ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc) $ ssh node-user@${INSTANCE_IP} ``` @@ -142,24 +141,23 @@ NOTE: If you get an offending ECDSA key error, use the suggested removal command NOTE: If you get the error `Permission denied (publickey).`, this probably means that your ssh-agent no longer has the node-user private key added. This easily happens if the Google Cloud Shell goes to sleep and wipes out your session. Check via issuing `ssh-add -l`. You should see something like `2048 SHA256:bII5VsQY3fCWXEai0lUeChEYPaagMXun3nB9U2eoUEM /home/betz4871/.ssh/node-user (RSA)`. If you do not, re-issue the command `ssh-add ~/.ssh/node-user` and re-confirm with `ssh-add -l`. -Verify git, nodejs, npm, and the node-svc app are installed. Do you understand how they got there? (Your results may be slightly different, but if you get errors, investigate or ask for help): +Verify git, nodejs, and npmare installed. Do you understand how they got there? (Your results may be slightly different, but if you get errors, investigate or ask for help): ```bash -$ git --version -git version 2.7.4 -$ nodejs -v -v4.2.6 -$ npm -v -3.5.2 -$ ls node-svc-v1/ -LICENSE node_modules package.json README.md run.sh server.js test.sh +node-user@node-svc:~$ npm -v +6.14.4 +node-user@node-svc:~$ node -v +v10.19.0 +node-user@node-svc:~$ git --version +git version 2.25.1 ``` - -Run server: +Run the installation script, and then the server: ```bash +$ chmod +x *.sh +$ sudo ./install.sh $ sudo nodejs node-svc-v1/server.js & ``` diff --git a/docs/05-terraform.md b/docs/05-terraform.md index acbafe1..7d4082f 100644 --- a/docs/05-terraform.md +++ b/docs/05-terraform.md @@ -1,8 +1,8 @@ # Terraform -In the previous lab, you used Packer to make your system configuration faster and more reliable. But we still have a lot to improve. +In the previous lab, you used scripts to make your system configuration faster and more reliable. But we still have a lot to improve. -In this lab, we're going to learn about another IaC tool by HashiCorp called [Terraform](https://www.terraform.io/). +In this lab, we're going to learn about the IaC tool by HashiCorp called [Terraform](https://www.terraform.io/). ## Intro @@ -16,7 +16,7 @@ We do it via a `gcloud` command like this: ```bash $ gcloud compute instances create node-svc \ - --image-family node-svc-base \ + --image-family ubuntu-minimal-2004-lts \ --boot-disk-size 10GB \ --machine-type f1-micro ``` @@ -136,7 +136,7 @@ We did provisioning via Terraform, but we still need to install and start our ap ```bash $ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc) # get IP of VM -$ scp -r install.sh node-user@${INSTANCE_IP}:/home/node-user # copy install script +$ scp -r ../03-script/install.sh node-user@${INSTANCE_IP}:/home/node-user # copy install script $ rsh ${INSTANCE_IP} -l node-user chmod +x /home/node-user/install.sh # set permissions $ rsh ${INSTANCE_IP} -l node-user /home/node-user/install.sh # install app $ rsh ${INSTANCE_IP} -l node-user sudo nodejs /home/node-user/node-svc-v1/server.js & # run app diff --git a/docs/06-ansible.md b/docs/06-ansible.md index f7f9da9..d912cc1 100644 --- a/docs/06-ansible.md +++ b/docs/06-ansible.md @@ -45,7 +45,7 @@ Create a new directory called `06-ansible` inside your `iac-tutorial` repo, whic ## Provision compute resources -Start a VM and create other GCP resources for running your application applying Terraform configuration you wrote in the previous lab: +Start a VM and create other GCP resources for running your application applying Terraform configuration you wrote in the previous lab (destroy first if you have some still running): ```bash $ cd ./05-terraform # adapt this command as necessary to get to the directory @@ -103,7 +103,7 @@ Let's create a file called `deploy.yml` ("deploy" including both installation an - name: NPM install express and initialize app # see https://docs.ansible.com/ansible/latest/modules/npm_module.html npm: - name: express + name: express global: yes - name: Install packages based on package.json. diff --git a/docs/08-docker.md b/docs/08-docker.md index 4adbfa0..992a47f 100644 --- a/docs/08-docker.md +++ b/docs/08-docker.md @@ -30,7 +30,7 @@ Verify that the version of Docker Engine is => 17.09.0: $ docker -v ``` -## Create Dockerfile +## (FOR ALL) Create Dockerfile You describe a container image that you want to create in a special file called **Dockerfile**. @@ -43,37 +43,34 @@ Dockerfile contains `instructions` on how the image should be built. Here are so * `WORKDIR` changes the working directory of the container to a specified path. It basically works like a `cd` command on Linux. * `CMD` sets a default command, which will be executed when a container starts. This should be a command to start your application. -Let's use these instructions to create a Docker container image for our raddit application. +Let's use these instructions to create a Docker container image for our node-svc application. -Inside your `my-iac-tutorial` directory, create a text file called `Dockerfile` with the following content: +Inside your `my-iac-tutorial` directory, create a directory called `08-docker`, and in it a text file called `Dockerfile` with the following content: ``` -# Use base image with Ruby installed -FROM ruby:2.3 - -# install required system packages -RUN apt-get update -qq && \ - apt-get install -y build-essential - -# create application directory and install dependencies -ENV APP_HOME /app -RUN mkdir $APP_HOME -WORKDIR $APP_HOME -COPY raddit-app/Gemfile* $APP_HOME/ -RUN bundle install - -# Copy the application code to the container -ADD raddit-app/ $APP_HOME -# Run "puma" command on container's start -CMD ["puma"] +FROM node:11 +# Create app directory +WORKDIR /app +# Install app dependencies +# A wildcard is used to ensure both package.json AND package-lock.json are copied +# where available (npm@5+) +COPY package*.json ./ +RUN npm install +RUN npm install express +# If you are building your code for production +# RUN npm ci --only=production +# Bundle app source +COPY . /app +EXPOSE 3000 +CMD [ "node", "server.js" ] ``` This Dockerfile repeats the steps that we did multiple times by now to configure a running environment for our application and run it. -We first choose an image that already contains Ruby of required version: +We first choose an image that already contains Node of required version: ``` # Use base image with Ruby installed -FROM ruby:2.3 +FROM node:11 ``` The base image is downloaded from Docker official registry (storage of images) called [Docker Hub](https://hub.docker.com/). @@ -81,39 +78,38 @@ The base image is downloaded from Docker official registry (storage of images) c We then install required system packages and application dependencies: ``` -# install required system packages -RUN apt-get update -qq && \ - apt-get install -y build-essential +# Install app dependencies +# A wildcard is used to ensure both package.json AND package-lock.json are copied +# where available (npm@5+)COPY package*.json ./ +RUN npm install +RUN npm install express +``` + +Then we copy the application itself. -# create application home directory and install dependencies -ENV APP_HOME /app -RUN mkdir $APP_HOME -WORKDIR $APP_HOME -COPY raddit-app/Gemfile* $APP_HOME/ -RUN bundle install +``` +# create application home directory and copy files +COPY . /app ``` -Then we copy the directory with application code and specify a default command that should be run when a container from this image starts: +Then we specify a default command that should be run when a container from this image starts: ``` -# Copy the application code to the container -ADD raddit-app/ $APP_HOME -# Run "puma" command on container's start -CMD ["puma"] +CMD [ "node", "server.js" ] ``` ## Build Container Image -Once you defined how your image should be built, run the following command inside your `my-iac-tutorial` directory to create a container image for raddit application: +Once you defined how your image should be built, run the following command inside your `my-iac-tutorial` directory to create a container image for the node-svc application: ```bash -$ docker build --tag raddit . +$ docker build -t /node-svc-v1 . ``` -The resulting image will be named `raddit`. Find it in the list of your local images: +The resulting image will be named `node-svc`. Find it in the list of your local images: ```bash -$ docker images | grep raddit +$ docker images | grep node-svc ``` ## Bridge Network From b21c891ce8c6ea5d3aa58d0603e5117fdff3229e Mon Sep 17 00:00:00 2001 From: Charles Betz Date: Sun, 16 Aug 2020 16:10:56 -0500 Subject: [PATCH 91/93] 08 --- docs/08-docker.md | 106 +++++++++------------------------------------- 1 file changed, 20 insertions(+), 86 deletions(-) diff --git a/docs/08-docker.md b/docs/08-docker.md index 992a47f..bd10c2a 100644 --- a/docs/08-docker.md +++ b/docs/08-docker.md @@ -69,7 +69,7 @@ This Dockerfile repeats the steps that we did multiple times by now to configure We first choose an image that already contains Node of required version: ``` -# Use base image with Ruby installed +# Use base image with node installed FROM node:11 ``` @@ -103,7 +103,7 @@ CMD [ "node", "server.js" ] Once you defined how your image should be built, run the following command inside your `my-iac-tutorial` directory to create a container image for the node-svc application: ```bash -$ docker build -t /node-svc-v1 . +$ docker build -t /node-svc-v1 . ``` The resulting image will be named `node-svc`. Find it in the list of your local images: @@ -111,99 +111,30 @@ The resulting image will be named `node-svc`. Find it in the list of your local ```bash $ docker images | grep node-svc ``` +At your option, you can save your build command in a script, such as `build.sh`. -## Bridge Network - -We are going to run multiple containers in this setup. To allow containers communicate with each other by container names, we'll create a [user-defined bridge network](https://docs.docker.com/engine/userguide/networking/#user-defined-networks): - -```bash -$ docker network create raddit-network -``` - -Verify that the network was created: +Now, run the container: ```bash -$ docker network ls +$ docker run -d -p 8081:3000 /node-svc-v1 ``` -## MongoDB Container +Notice the "8081:3000" syntax. This means that while the container is running on port 3000 internally, it is externally exposed via port 8081. -We shouldn't forget that we also need a MongoDB for our application to work. - -The philosophy behind containers is that we create one container per process. So we'll run MongoDB in another container. - -We will use a public image from Docker Hub to run a MongoDB container alongside raddit application container. However, I recommend you for the sake of practice write a Dockerfile for MongoDB and create your own image. - -Because MongoDB is a stateful service, we'll first create a named volume for it to persist the data beyond the container removal. - -```bash -$ docker volume create mongo-data -``` - -Check that volume was created: - -```bash -$ docker volume ls | grep mongo-data -``` - -Now run the following command to download a MongodDB image and start a container from it: - -```bash -$ docker run --name mongo-database \ - --volume mongo-data:/data/db \ - --network raddit-network \ - --detach mongo:3.2 -``` +Again, you may wish to save this in a script, such as `run.sh`. -Verify that the container is running: +Now, test the container: ```bash -$ docker container ls +$ curl localhost:8081 +Successful request. ``` -## Start Application Container - -Start the application container from the image you've built: - -```bash -$ docker run --name raddit-app \ - --env DATABASE_HOST=mongo-database \ - --network raddit-network \ - --publish 9292:9292 \ - --detach raddit -``` - -Note, how we also passed an environment variable with the command to the application container. Since MongoDB is not reachable at `localhost` as it was in the previous labs, [we need to pass the environment variable with MongoDB address](https://github.com/Artemmkin/iac-tutorial/blob/master/raddit-app/app.rb#L11) to tell our application where to connect. Automatic DNS resolution of container names within a user-defined network makes it possible to simply pass the name of a MongoDB container instead of an IP address. - -Port mapping option (`--publish`) that we passed to the command is used to make the container reachable to the outsite world. - -## Access Application - -To access via the Google Cloud Shell, use the Web Preview: - -![](../img/webPreview.png) - -AT THIS WRITING, you cannot select port 9292. Select any offered port: - -![](../img/webPreviewPort.png) - -You will receive an error. Notice your URL which should look something like this: - -`https://8080-dot-8658285-dot-devshell.appspot.com/?authuser=0` - -The first four digits after the `https://` represent the desired port. Change this to 9292, for example: - -`https://9292-dot-8658285-dot-devshell.appspot.com/?authuser=0` - -(Do not try to use that URL. Modify the one provided you.) - -You should now be able to see the application. - -(If you are running this tutorial locally, the application should be accessible to your at http://localhost:9292) +Again, you may wish to save this in a script, such as `test.sh`. ## Save and commit the work -Save and commit the `Dockerfile` created in this lab into your `iac-tutorial` repo. +Save and commit the files created in this lab. ## Conclusion @@ -211,13 +142,16 @@ In this lab, you adopted containers for running your application. This is a diff We describe the configuration of our container image in a Dockerfile using Dockerfile's syntax. We then save that Dockefile in our application repository. This way we can build the application image consistently across any environments. -Destroy the current playground before moving on to the next lab. +Destroy the current playground before moving on to the next lab, through `docker ps`, `docker kill`, `docker images`, and `docker rmi`. In the example below, the container is named "beautiful_pascal". Yours will be different. Follow the example, substituting yours. ```bash -$ docker rm -f mongo-database -$ docker rm -f raddit-app -$ docker volume rm mongo-data -$ docker network rm raddit-network +$ docker ps +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +64e60b7b0c81 charlestbetz/node-svc-v1 "docker-entrypoint.s…" 10 minutes ago Up 10 minutes 0.0.0.0:8081->3000/tcp beautiful_pascal +$ docker kill beautiful_pascal +$ docker images +# returns list of your images +$ docker rmi -f ``` Next: [Docker Compose](09-docker-compose.md) From 9f94d9bda987f1bdbc1c5555644138cbd0e16bbb Mon Sep 17 00:00:00 2001 From: Charles Betz Date: Sat, 22 Aug 2020 11:03:31 -0500 Subject: [PATCH 92/93] adoc conversion --- docs/00-introduction.adoc | 23 ++ docs/01-prerequisites.adoc | 38 +++ docs/02-manual-operations.adoc | 195 +++++++++++++ docs/03-scripts.adoc | 246 +++++++++++++++++ docs/04-packer.adoc | 244 +++++++++++++++++ docs/05-terraform.adoc | 344 +++++++++++++++++++++++ docs/06-ansible.adoc | 300 ++++++++++++++++++++ docs/07-vagrant.adoc | 309 +++++++++++++++++++++ docs/08-docker.adoc | 189 +++++++++++++ docs/09-docker-compose.adoc | 178 ++++++++++++ docs/09-docker-compose.md | 3 + docs/10-kubernetes.adoc | 486 +++++++++++++++++++++++++++++++++ docs/50-what-is-iac.adoc | 22 ++ docs/convert.sh | 8 + docs/convert2.sh | 4 + 15 files changed, 2589 insertions(+) create mode 100644 docs/00-introduction.adoc create mode 100644 docs/01-prerequisites.adoc create mode 100644 docs/02-manual-operations.adoc create mode 100644 docs/03-scripts.adoc create mode 100644 docs/04-packer.adoc create mode 100644 docs/05-terraform.adoc create mode 100644 docs/06-ansible.adoc create mode 100644 docs/07-vagrant.adoc create mode 100644 docs/08-docker.adoc create mode 100644 docs/09-docker-compose.adoc create mode 100644 docs/10-kubernetes.adoc create mode 100644 docs/50-what-is-iac.adoc create mode 100755 docs/convert.sh create mode 100755 docs/convert2.sh diff --git a/docs/00-introduction.adoc b/docs/00-introduction.adoc new file mode 100644 index 0000000..f0dce87 --- /dev/null +++ b/docs/00-introduction.adoc @@ -0,0 +1,23 @@ += Introduction + +Let's dream for a little bit... + +Imagine that you're a young developer who developed a web application. +You run and test your application locally and everything works great, which makes you very happy. +You believe that this is going to blow the minds of Internet users and bring you a lot of money. + +Then you realize that there is a small problem. +You ask yourself a question: "How do I make my application available to the Internet users?" + +You're thinking that you can't run the application locally all the time, because your old laptop will become slow for other tasks and will probably crash if a lot of users will be using your app at the same time. +Besides, your ISP changes randomly the public IP for your router, so you don't know on which IP address your application will be accessible to the public at any given moment. + +You start realizing that the problem you're facing is not as small as you thought. +In fact, there is a whole new craft for you to learn in IT world about running software applications and making sure they are always available to the users. + +The craft is called *IT operations*. +And in almost every IT department, there is an operations (Ops) team who manages the platform where the applications are running. + +The tutorial you are about to begin will give you, a young developer, a bit of a glance into what operations work look like and how you can do this work more efficiently by using *Infrastructure as Code* approach. + +Next: xref:01-prerequisites.adoc[Prerequisites] diff --git a/docs/01-prerequisites.adoc b/docs/01-prerequisites.adoc new file mode 100644 index 0000000..548a4c3 --- /dev/null +++ b/docs/01-prerequisites.adoc @@ -0,0 +1,38 @@ += Prerequisites + +== Google Cloud Platform + +In this tutorial, we use the https://cloud.google.com/[Google Cloud Platform] to provision the compute infrastructure. +You have already signed up. + +Start in the Google Cloud Shell. +https://cloud.google.com/shell/docs/using-cloud-shell[(review)] + +== Google Cloud Platform + +=== Set a Default Project, Compute Region and Zone + +This tutorial assumes a default compute region and zone have been configured. + +Set a default compute region appropriate to your location (https://cloud.google.com/compute/docs/regions-zones[GCP regions and zones]): + +[source,bash] +---- +$ gcloud config set compute/region us-central1 +---- + +Set a default compute zone appropriate to the zone: + +[source,bash] +---- +$ gcloud config set compute/zone us-central1-c +---- + +Verify the configuration settings: + +[source,bash] +---- +$ gcloud config list +---- + +Next: xref:02-manual-operations.adoc[Manual operations] diff --git a/docs/02-manual-operations.adoc b/docs/02-manual-operations.adoc new file mode 100644 index 0000000..7d0eb27 --- /dev/null +++ b/docs/02-manual-operations.adoc @@ -0,0 +1,195 @@ += Manual Operations + +To better understand the `Infrastructure as Code` (`IaC`) concept, we will first define the problem we are facing and deal with it with manually to get our hands dirty and see how things work overall. + +== Intro + +Imagine you have developed a new cool application called https://github.com/dm-academy/node-svc-v1[node-svc]. + +You want to run your application on a dedicated server and make it available to the Internet users. + +You heard about the `public cloud` thing, which allows you to provision compute resources and pay only for what you use. +You believe it's a great way to test your idea of an application and see if people like it. + +You've signed up for a free tier of https://cloud.google.com/[Google Cloud Platform] (GCP) and are about to start deploying your application. + +== Provision Compute Resources + +First thing we will do is to provision a virtual machine (VM) inside GCP for running the application. + +Use the following gcloud command in your terminal to launch a VM with Ubuntu 16.04 distro: + +[source,bash] +---- +$ gcloud compute instances create node-svc\ + --image-family ubuntu-minimal-2004-lts \ + --image-project ubuntu-os-cloud \ + --boot-disk-size 10GB \ + --machine-type f1-micro +---- + +== Create an SSH key pair + +Generate an SSH key pair for future connections to the VM instances (run the command exactly as it is): + +[source,bash] +---- +$ ssh-keygen -t rsa -f ~/.ssh/node-user -C node-user -P "" +---- + +Create an SSH public key for your project: + +[source,bash] +---- +$ gcloud compute project-info add-metadata \ + --metadata ssh-keys="node-user:$(cat ~/.ssh/node-user.pub)" +---- + +Check your ssh-agent is running: + +[source,bash] +---- +$ echo $SSH_AGENT_PID +---- + +If you get a number, it is running. +If you get nothing, then run: + +[source,bash] +---- +$ eval `ssh-agent` +---- + +Add the SSH private key to the ssh-agent: + + $ ssh-add ~/.ssh/node-user + +Verify that the key was added to the ssh-agent: + +[source,bash] +---- +$ ssh-add -l +---- + +== Install Application Dependencies + +To start the application, you need to first configure the environment for running it. + +Connect to the started VM via SSH using the following two commands: + +[source,bash] +---- +$ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc) +$ ssh node-user@${INSTANCE_IP} +---- + +Install Node and npm: + +[source,bash] +---- +$ +$ sudo apt-get install -y nodejs npm +---- + +Check the installed version of Node: + +[source,bash] +---- +$ node -v +---- + +Install `git`: + +[source,bash] +---- +$ sudo apt -y install git +---- + +Clone the application repo into the home directory of `node-user` user (reminder, how do you clone to the right location?): + +[source,bash] +---- +$ git clone https://github.com/dm-academy/node-svc-v1 +---- + +Navigate to the repo (`cd node-svc-v1`) and check out the 02 branch (matching this lesson) + +[source,bash] +---- +$ git checkout 02 +Branch 02 set up to track remote branch 02 from origin. +Switched to a new branch '02' +---- + +Initialize npm (Node Package Manager) and install express: + +[source,bash] +---- +$ npm install +$ npm install express +---- + +== Start the Application + +Look at the server.js file (`cat`). +We will discuss in class. + +Start the Node web server: + +[source,bash] +---- +$ nodejs server.js & +Running on 3000 +---- + +Test it: + +[source,bash] +---- +$ curl localhost:3000 +Successful request. +---- + +== Access the Application + +Open a firewall port the application is listening on (note that the following command should be run on the Google Cloud Shell): + +[source,bash] +---- +$ gcloud compute firewall-rules create allow-node-svc-tcp-3000 \ + --network default \ + --action allow \ + --direction ingress \ + --rules tcp:3000 \ + --source-ranges 0.0.0.0/0 +---- + +Get the public IP of the VM: + +[source,bash] +---- +$ gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc-instance +---- + +Now open your browser and try to reach the application at the public IP and port 3000. + +For example, I put in my browser the following URL http://104.155.1.152:3000, but note that you'll have your own IP address. + +== Conclusion + +Congrats! +You've just deployed your application. +It is running on a dedicated set of compute resources in the cloud and is accessible by a public IP. +Now Internet users can enjoy using your application. + +Now that you've got the idea of what sort of steps you have to take to deploy your code from your local machine to a virtual server running in the cloud, let's see how we can do it more efficiently. + +Destroy the current VM and firewall rule and move to the next step: + +[source,bash] +---- +$ gcloud compute instances delete -q node-svc +$ gcloud compute firewall-rules delete -q allow-node-svc-tcp-9292 +---- + +Next: xref:03-scripts.adoc[Scripts] diff --git a/docs/03-scripts.adoc b/docs/03-scripts.adoc new file mode 100644 index 0000000..c1742bf --- /dev/null +++ b/docs/03-scripts.adoc @@ -0,0 +1,246 @@ += Scripts + +In the previous lab, you deployed the https://github.com/dm-academy/node-svc[node-svc] application by connecting to a VM via SSH and running commands in the terminal one by one. +In this lab, we'll try to automate this process a little by using `scripts`. + +== Intro + +Now think about what happens if your application becomes so popular that one virtual machine can't handle all the load of incoming requests. +Or what happens when your application somehow crashes? +Debugging a problem can take a long time and it would most likely be much faster to launch and configure a new VM than trying to fix what's broken. + +In all of these cases we face the task of provisioning new virtual machines, installing the required software and repeating all of the configurations we've made in the previous lab over and over again. + +Doing it manually is boring, error-prone and time-consuming. + +The most obvious way for improvement is using Bash scripts which allow us to run sets of commands put in a single file. +So let's try this. + +== Infrastructure as Code project + +Starting from this lab, we're going to use a git repo for saving all the work done in this tutorial. + +Go to your Github account and create a new repository called iac-repo. +No README or .gitignore. +Copy the URL. + +Clone locally: + +[source,bash] +---- +$ git clone +---- + +Create a directory for this lab: + +[source,bash] +---- +$ cd iac-repo +$ mkdir 03-script +$ cd 03-script +---- + +To push your changes up to Github: + +[source,bash] +---- +$ git add . -A +$ git commit -m "first lab 03 commit" # should be relevant to the changes you made +$ git push origin master +---- + +Always issue these commands several times during each session. + +== Provisioning script + +We can automate the process of creating the VM and the firewall rule. + +In the `script` directory create a script `provision.sh`: + +[source,bash] +---- +#!/bin/bash +# add new VM +gcloud compute instances create node-svc \ + --image-family ubuntu-minimal-2004-lts \ + --image-project ubuntu-os-cloud \ + --boot-disk-size 10GB \ + --machine-type f1-micro + +# add firewall rule +gcloud compute firewall-rules create allow-node-svc-tcp-3000 \ + --network default \ + --action allow \ + --direction ingress \ + --rules tcp:3000 \ + --source-ranges 0.0.0.0/0 +---- + +Run it in the Google Cloud Shell: + +[source,bash] +---- +$ chmod +x provision.sh # changing permissions +$ ./provision.sh # you have to include the './' +---- + +You should see results similar to: + +[source,bash] +---- +WARNING: You have selected a disk size of under [200GB]. This may result in poor I/O performance. For more information, see: https://developers.google.com/compute/docs/disks#performance. +Created [https://www.googleapis.com/compute/v1/projects/proven-sum-252123/zones/us-central1-c/instances/node-svc]. +NAME ZONE MACHINE_TYPE PREEMPTIBLE INTERNAL_IP EXTERNAL_IP STATUS +node-svc us-central1-c n1-standard-1 10.128.15.202 34.69.206.6 RUNNING +Creating firewall...⠹Created [https://www.googleapis.com/compute/v1/projects/proven-sum-252123/global/firewalls/allow-node-svc-3000]. +Creating firewall...done. +NAME NETWORK DIRECTION PRIORITY ALLOW DENY DISABLED +allow-node-svc-3000 default INGRESS 1000 tcp:3000 False +---- + +== Installation script + +Before we can run our application, we need to create a running environment for it by installing dependent packages and configuring the OS. +Then we copy the application, initialize NPM and download express.js, and start the server. + +We are going to use the same commands we used before to do that, but this time, instead of running commands one by one, we'll create a `bash script` to save us some struggle. + +In the `03-script` directory create bash script `config.sh` to install node, npm, express, and git. +Create a script `install.sh` to download the app and initialize node. + +[source,bash] +---- +#!/bin/bash +set -e # exit immediately if anything returns non-zero. See https://www.javatpoint.com/linux-set-command + +echo " ----- install node, npm, git ----- " +apt-get update +apt-get install -y nodejs npm git +---- + +[source,bash] +---- +#!/bin/bash +set -e # exit immediately if anything returns non-zero. See https://www.javatpoint.com/linux-set-command + +echo " ----- download, initialize, and run app ----- " +git clone https://github.com/dm-academy/node-svc-v1 +cd node-svc-v1 +git checkout 02 +npm install +npm install express +---- + +NOTE: Why two scripts? +Discuss in class. + +== Run the scripts + +Copy the script to the created VM: + +[source,bash] +---- +$ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc) +$ scp -r config.sh install.sh node-user@${INSTANCE_IP}:/home/node-user +---- + +If sucessful, you should see something like: + +[source,bash] +---- +config.sh 100% 214 279.9KB/s 00:00 +install.sh 100% 214 279.9KB/s 00:00 +---- + +NOTE: If you get an `offending ECDSA key` error, use the suggested removal command. + +NOTE: If you get the error `Permission denied (publickey).`, this probably means that your ssh-agent no longer has the node-user private key added. +This easily happens if the Google Cloud Shell goes to sleep and wipes out your session. +Check via issuing `ssh-add -l`. + +If you get a message to the effect that your agent is not running, type `eval `ssh-agent`` and then `ssh-add -l`. + +You should see something like `2048 SHA256:bII5VsQY3fCWXEai0lUeChEYPaagMXun3nB9U2eoUEM /home/betz4871/.ssh/node-user (RSA)`. +If you do not, re-issue the command `ssh-add ~/.ssh/node-user` and re-confirm with `ssh-add -l`. + +Connect to the VM via SSH: + +[source,bash] +---- +$ ssh node-user@${INSTANCE_IP} +---- + +Have a look at what's in the directory (use `ls` and `cat`). +Do you understand exactly how it got there? +If you do not, ask. + +Run the script and launch the server: + +[source,bash] +---- +$ chmod +x *.sh +$ sudo ./config.sh && ./install.sh # running 2 commands on one line +$ sudo nodejs node-svc-v1/server.js & +---- + +The last output should be `Running on 3000`. +You may need to hit Return or Enter to get a command prompt. + +To test that the server is running locally, type: + +[source,bash] +---- +$ curl localhost:3000 +---- + +You should receive this: + +[source,bash] +---- +Successful request. +---- + +== Access the Application + +Access the application in your browser by its public IP (don't forget to specify the port 3000). + +Open another terminal and run the following command to get a public IP of the VM: + +[source,bash] +---- +$ gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc +---- + +== Destroy (de-provision) the resources by script + +In the `provision` directory create a script `deprovision.sh`. + +[source,bash] +---- +#!/bin/bash +gcloud compute instances delete -q node-svc +gcloud compute firewall-rules delete -q allow-node-svc-tcp-3000 +---- + +Set permissions correctly (see previous) and execute. +You should get results like: + +`+bash Deleted [https://www.googleapis.com/compute/v1/projects/proven-sum-252123/zones/us-central1-c/instances/node-svc]. +Deleted [https://www.googleapis.com/compute/v1/projects/proven-sum-252123/global/firewalls/allow-node-svc-tcp-3000].+` + +== Save and commit the work + +Save and commit the scripts created in this lab into your `iac-tutorial` repo. + +== Conclusion + +Scripts helped us to save some time and effort of manually running every command one by one to configure the system and start the application. + +The process of system configuration becomes more or less standardized and less error-prone, as you put commands in the order they should be run and test it to ensure it works as expected. + +It's also a first step we've made in the direction of automating operations work. + +But scripts are not suitable for every operations task and have many downsides. +We'll discuss more on that in the next labs. + +Next: xref:04-packer.adoc[Packer] diff --git a/docs/04-packer.adoc b/docs/04-packer.adoc new file mode 100644 index 0000000..5ecdf88 --- /dev/null +++ b/docs/04-packer.adoc @@ -0,0 +1,244 @@ += Packer + +Scripts helped us speed up the process of system configuration, and made it more reliable compared to doing everything manually, but there are still ways for improvement. + +In this lab, we're going to take a look at the first IaC tool in this tutorial called https://www.packer.io/[Packer] and see how it can help us improve our operations. + +== Intro + +Remember how in the second lab we had to make install nodejs, npm, and even git on the VM so that we could clone the application repo? +Did it surprise you that `git` was not already installed on the system? + +Imagine how nice it would be to have required packages like nodejs and npm preinstalled on the VM we provision, or have necessary configuration files come with the image, too. +This would require even less time and effort from us to configure the system and run our application. + +Luckily, we can create custom machine images with required configuration and software installed using Packer, an IaC tool by Hashicorp. +Let's check it out. + +== Install Packer + +https://www.packer.io/downloads.html[Download] and install Packer onto your system (this means the Google Cloud Shell). +You will need to figure this out. + +If you have issues, consult https://github.com/dm-academy/iac-tutorial-rsrc/blob/master/packer/install-packer.sh[this script]. + +Check the version to verify that it was installed: + +[source,bash] +---- +$ packer -v +---- + +== Infrastructure as Code project + +Create a new directory called `04-packer` inside your `iac-tutorial` repo, which we'll use to save the work done in this lab. + +== Define image builder + +The way Packer works is simple. +It starts a VM with specified characteristics, configures the operating system and installs the software you specify, and then it creates a machine image from that VM. + +The part of packer responsible for starting a VM and creating an image from it is called https://www.packer.io/docs/builders/index.html[builder]. + +So before using packer to create images, we need to define a builder configuration in a JSON file (which is called *template* in Packer terminology). + +Create a `node-svc-base-image.json` file inside the `packer` directory with the following content (make sure to change the project ID, and also the zone in case it's different): + +[source,json] +---- +{ + "builders": [ + { + "type": "googlecompute", + "project_id": "YOUR PROJECT HERE. YOU MUST CHANGE THIS", + "zone": "us-central1-c", + "machine_type": "f1-micro", + "source_image_family": "ubuntu-minimal-2004-lts", + "image_name": "node-svc-base-{{isotime \"2006-01-02 03:04:05\"}}", + "image_family": "node-svc-base", + "image_description": "Ubuntu 16.04 with git, nodejs, npm preinstalled", + "ssh_username": "node-user" + } + ] +} +---- + +This template describes where and what type of a VM to launch for image creation (`type`, `project_id`, `zone`, `machine_type`, `source_image_family`). +It also defines image saving configuration such as under which name (`image_name`) and image family (`image_family`) the resulting image should be saved and what description to give it (`image_description`). +SSH user configuration is used by provisioners which will talk about later. + +Validate the template: + +[source,bash] +---- +$ packer validate node-svc-base-image.json +---- + +== Define image provisioner + +As we already mentioned, builders are only responsible for starting a VM and creating an image from that VM. +The real work of system configuration and installing software on the running VM is done by another Packer component called *provisioner*. + +Add a https://www.packer.io/docs/provisioners/shell.html[shell provisioner] to your template to run the `deploy.sh` script you created in the previous lab. + +Your template should look similar to this one: + +[source,json] +---- +{ + "builders": [ + { + "type": "googlecompute", + "project_id": "YOUR PROJECT HERE. YOU MUST CHANGE THIS", + "zone": "us-central1-c", + "machine_type": "f1-micro", + "source_image_family": "ubuntu-1604-lts", + "image_name": "node-svc-base-{{isotime `20200901-000001`}}", + "image_family": "node-svc-base", + "image_description": "Ubuntu 16.04 with git, nodejs, npm, and node-svc preinstalled", + "ssh_username": "node-user" + } + ], + "provisioners": [ + { + "type": "shell", + "script": "{{template_dir}}/../03-script/config.sh", + "execute_command": "sudo {{.Path}}" + } + ] +} +---- + +Make sure the template is valid: + +[source,bash] +---- +$ packer validate ./packer/node-base-image.json +---- + +== Create custom machine image + +Build the image for your application: + +[source,bash] +---- +$ packer build node-svc-base-image.json +---- + +If you go to the https://console.cloud.google.com/compute/images[Compute Engine Images] page you should see your new custom image. + +== Launch a VM with your custom built machine image + +Once the image is built, use it as a boot disk to start a VM: + +[source,bash] +---- +$ gcloud compute instances create node-svc \ + --image-family node-svc-base \ + --boot-disk-size 10GB \ + --machine-type f1-micro +---- + +== Deploy Application + +Copy the installation script to the VM: + +$ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc) $ scp -r ../03-script/install.sh node-user@$\{INSTANCE_IP}:/home/node-user + +Connect to the VM via SSH: + +[source,bash] +---- +$ ssh node-user@${INSTANCE_IP} +---- + +NOTE: If you get an offending ECDSA key error, use the suggested removal command. + +NOTE: If you get the error `Permission denied (publickey).`, this probably means that your ssh-agent no longer has the node-user private key added. +This easily happens if the Google Cloud Shell goes to sleep and wipes out your session. +Check via issuing `ssh-add -l`. +You should see something like `2048 SHA256:bII5VsQY3fCWXEai0lUeChEYPaagMXun3nB9U2eoUEM /home/betz4871/.ssh/node-user (RSA)`. +If you do not, re-issue the command `ssh-add ~/.ssh/node-user` and re-confirm with `ssh-add -l`. + +Verify git, nodejs, and npmare installed. +Do you understand how they got there? +(Your results may be slightly different, but if you get errors, investigate or ask for help): + +[source,bash] +---- +node-user@node-svc:~$ npm -v +6.14.4 +node-user@node-svc:~$ node -v +v10.19.0 +node-user@node-svc:~$ git --version +git version 2.25.1 +---- + +Run the installation script, and then the server: + +[source,bash] +---- +$ chmod +x *.sh +$ sudo ./install.sh +$ sudo nodejs node-svc-v1/server.js & +---- + +== Access Application + +Manually re-create the firewall rule: + +[source,bash] +---- +$ gcloud compute firewall-rules create allow-node-svc-tcp-3000 \ + --network default \ + --action allow \ + --direction ingress \ + --rules tcp:3000 \ + --source-ranges 0.0.0.0/0 +---- + +Open another terminal and run the following command to get a public IP of the VM: + +[source,bash] +---- +$ gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc +---- + +Access the application in your browser by its public IP (don't forget to specify the port 3000). + +== De-provision + +[source,bash] +---- +$ ../03-script/deprovision.sh #notice path +---- + +== Save and commit the work + +Save and commit the packer template created in this lab into your `iac-tutorial` repo. + +== Learning more about Packer + +Packer configuration files are called templates for a reason. +They often get parameterized with https://www.packer.io/docs/templates/user-variables.html[user variables]. +This could be very helpful since you can create multiple machine images with different configurations for different purposes using one template file. + +Adding user variables to a template is easy, follow the https://www.packer.io/docs/templates/user-variables.html[documentation] on how to do that. + +== Immutable infrastructure + +By putting everything inside the image including the application, we have achieved an https://martinfowler.com/bliki/ImmutableServer.html[immutable infrastructure]. +It is based on the idea `we build it once, and we never change it`. + +It has advantages of spending less time (zero in this case) on system configuration after VM's start, and prevents *configuration drift*, but it's also not easy to implement. + +== Conclusion + +In this lab you've used Packer to create a custom machine image for running your application. + +Its advantages include: + +* `It requires less time and effort to configure a new VM for running the application` +* `System configuration becomes more reliable.` When we start a new VM to deploy the application, we know for sure that it has the right packages installed and configured properly, since we built and tested the image. + +Next: xref:05-terraform.adoc[Terraform] diff --git a/docs/05-terraform.adoc b/docs/05-terraform.adoc new file mode 100644 index 0000000..d54fbaa --- /dev/null +++ b/docs/05-terraform.adoc @@ -0,0 +1,344 @@ += Terraform + +In the previous lab, you used scripts to make your system configuration faster and more reliable. +But we still have a lot to improve. + +In this lab, we're going to learn about the IaC tool by HashiCorp called https://www.terraform.io/[Terraform]. + +== Intro + +Think about your current operations... + +Do you see any problems you may have, or any ways for improvement? + +Remember, that each time we want to deploy an application, we have to `provision` compute resources first, that is to start a new VM. + +We do it via a `gcloud` command like this: + +[source,bash] +---- +$ gcloud compute instances create node-svc \ + --image-family ubuntu-minimal-2004-lts \ + --boot-disk-size 10GB \ + --machine-type f1-micro +---- + +At this stage, it doesn't seem like there are any problems with this. +But, in fact, there are. + +Infrastructure for running your services and applications could be huge. +You might have tens, hundreds or even thousands of virtual machines, hundreds of firewall rules, multiple VPC networks and load balancers. +Additionally, the infrastructure could be split between multiple teams. +Such infrastructure looks, and is, very complex and yet should be run and managed in a consistent and predictable way. + +If we create and change infrastructure components using the Web User Interface (UI) Console or even the gcloud command ine interface (CLI) tool, over time we won't be able to describe exactly in which `state` our infrastructure is in right now, meaning `we lose control over it`. + +This happens because you tend to forget what changes you've made a few months ago and why you made them. +If multiple people across multiple teams are managing infrastructure, this makes things even worse. + +So we see here 2 clear problems: + +* we don't know the current state of our infrastructure +* we can't control the changes + +The second problem is dealt by source control tools like `git`, while the first one is solved by using tools like Terraform. +Let's find out how. + +== Terraform + +Terraform is already installed on Google Cloud Shell. + +If you want to install it on a laptop or VM, you can https://www.terraform.io/downloads.html[download here]. + +Make sure Terraform version is \=> 0.11.0: + +[source,bash] +---- +$ terraform -v +---- + +== Infrastructure as Code project + +Create a new directory called `05-terraform` inside your `iac-tutorial` repo, which we'll use to save the work done in this lab. + +== Describe VM instance + +_Terraform allows you to describe the desired state of your infrastructure and makes sure your desired state meets the actual state._ + +Terraform uses https://www.terraform.io/docs/configuration/resources.html[*resources*] to describe different infrastructure components. +If you want to use Terraform to manage an infrastructure component, you should first make sure there is a resource for that component for that particular platform. + +Let's use Terraform syntax to describe a VM instance that we want to be running. + +Create a Terraform configuration file called `main.tf` inside the `05-terraform` directory with the following content: + +---- +resource "google_compute_instance" "node-svc" { + name = "node-svc" + machine_type = "f1-micro" + zone = "us-central1-c" + + # boot disk specifications + boot_disk { + initialize_params { + image = "node-svc-base" // use image built with Packer + } + } + + # networks to attach to the VM + network_interface { + network = "default" + access_config {} // use ephemeral public IP + } +} +---- + +Here we use https://www.terraform.io/docs/providers/google/r/compute_instance.html[google_compute_instance] resource to manage a VM instance running in Google Cloud Platform. + +== Define Resource Provider + +One of the advantages of Terraform over other alternatives like https://aws.amazon.com/cloudformation/?nc1=h_ls[CloudFormation] is that it's `cloud-agnostic`, meaning it can work with many different cloud providers like AWS, GCP, Azure, or OpenStack. +It can also work with resources of different services like databases (e.g., PostgreSQL, MySQL), orchestrators (Kubernetes, Nomad) and https://www.terraform.io/docs/providers/[others]. + +This means that Terraform has a pluggable architecture and the pluggable component that allows it to work with a specific platform or service is called *provider*. + +So before we can actually create a VM using Terraform, we need to define a configuration of a https://www.terraform.io/docs/providers/google/index.html[google cloud provider] and download it on our system. + +Create another file inside `terraform` folder and call it `providers.tf`. +Put provider configuration in it: + +---- +provider "google" { + version = "~> 2.5.0" + project = "YOU MUST PUT YOUR PROJECT NAME HERE" + region = "us-central1-c" +} +---- + +Make sure to change the `project` value in provider's configuration above to your project's ID. +You can get your default project's ID by running the command: + +[source,bash] +---- +$ gcloud config list project +---- + +Now run the `init` command inside `terraform` directory to download the provider: + +[source,bash] +---- +$ terraform init +---- + +== Bring Infrastructure to a Desired State + +Once we described a desired state of the infrastructure (in our case it's a running VM), let's use Terraform to bring the infrastructure to this state: + +[source,bash] +---- +$ terraform apply +---- + +After Terraform ran successfully, use a gcloud command to verify that the machine was indeed launched: + +[source,bash] +---- +$ gcloud compute instances describe node-svc +---- + +== Deploy Application + +We did provisioning via Terraform, but we still need to install and start our application. +Let's do this remotely this time, instead of logging into the machine: + +[source,bash] +---- +$ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc) # get IP of VM +$ scp -r ../03-script/install.sh node-user@${INSTANCE_IP}:/home/node-user # copy install script +$ rsh ${INSTANCE_IP} -l node-user chmod +x /home/node-user/install.sh # set permissions +$ rsh ${INSTANCE_IP} -l node-user /home/node-user/install.sh # install app +$ rsh ${INSTANCE_IP} -l node-user sudo nodejs /home/node-user/node-svc-v1/server.js & # run app +---- + +NOTE: If you get an offending ECDSA key error, use the suggested removal command. + +NOTE: If you get the error `Permission denied (publickey).`, this probably means that your ssh-agent no longer has the node-user private key added. +This easily happens if the Google Cloud Shell goes to sleep and wipes out your session. +Check via issuing `ssh-add -l`. +You should see something like `2048 SHA256:bII5VsQY3fCWXEai0lUeChEYPaagMXun3nB9U2eoUEM /home/betz4871/.ssh/node-user (RSA)`. +If you do not, re-issue the command `ssh-add ~/.ssh/node-user` and re-confirm with `ssh-add -l`. + +Connect to the VM via SSH: + +[source,bash] +---- +$ ssh node-user@${INSTANCE_IP} +---- + +Check that servce is running, and then exit: + +[source,bash] +---- +node-user@node-svc:~$ curl localhost:3000 +Successful request. +node-user@node-svc:~$ exit +---- + +== Access the Application Externally7 + +Manually create the firewall rule: + +[source,bash] +---- +$ gcloud compute firewall-rules create allow-node-svc-tcp-3000 \ + --network default \ + --action allow \ + --direction ingress \ + --rules tcp:3000 \ + --source-ranges 0.0.0.0/0 +---- + +Open another terminal and run the following command to get a public IP of the VM: + +[source,bash] +---- +$ gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc +---- + +Access the application in your browser by its public IP (don't forget to specify the port 3000). + +== Add other GCP resources into Terraform + +Let's add ssh keys and the firewall rule into our Terraform configuration so that we know for sure those resources are present. + +First, delete the SSH project key and firewall rule: + +[source,bash] +---- +$ gcloud compute project-info remove-metadata --keys=ssh-keys +$ gcloud compute firewall-rules delete allow-node-svc-tcp-3000 +---- + +Make sure that your application became inaccessible via port 3000 and SSH connection with a private key of `node-user` fails. + +Then add appropriate resources into `main.tf` file. +Your final version of `main.tf` file should look similar to this (change the ssh key file path, if necessary): + +[source,bash] +---- +resource "google_compute_instance" "node-svc" { + name = "node-svc" + machine_type = "f1-micro" + zone = "us-central1-c" + + # boot disk specifications + boot_disk { + initialize_params { + image = "node-svc-base" // use image built with Packer + } + } + + # networks to attach to the VM + network_interface { + network = "default" + access_config {} // use ephemaral public IP + } +} + +resource "google_compute_project_metadata" "node-svc" { + metadata = { + ssh-keys = "node-user:${file("~/.ssh/node-user.pub")}" // path to ssh key file + } +} + +resource "google_compute_firewall" "node-svc" { + name = "allow-node-svc-tcp-3000" + network = "default" + allow { + protocol = "tcp" + ports = ["3000"] + } + source_ranges = ["0.0.0.0/0"] +} +---- + +Tell Terraform to apply the changes to bring the actual infrastructure state to the desired state we described: + +[source,bash] +---- +$ terraform apply +---- + +Using the same techniques as above, verify that the application became accessible again on port 3000 (locally and remotely) and SSH connection with a private key works. +Here's a new way to check it from the Google Cloud Shell (you don't ssh into the VM): + +[source,bash] +---- +$ curl $INSTANCE_IP:3000 +---- + +== Create an output variable + +We have frequntly used this gcloud command to retrieve a public IP address of a VM: + +[source,bash] +---- +$ gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc +---- + +We can tell Terraform to provide us this information using https://www.terraform.io/intro/getting-started/outputs.html[output variables]. + +Create another configuration file inside `terraform` directory and call it `outputs.tf`. +Put the following content in it: + +[source,json] +---- +output "node_svc_public_ip" { + value = "${google_compute_instance.node-svc.network_interface.0.access_config.0.nat_ip}" +} +---- + +Run terraform apply again, this time with auto approve: + +[source,bash] +---- +$ terraform apply -auto-approve + +google_compute_instance.node-svc: Refreshing state... [id=node-svc] +google_compute_firewall.node-svc: Refreshing state... [id=allow-node-svc-tcp-3000] +google_compute_project_metadata.node-svc: Refreshing state... [id=proven-sum-252123] +Apply complete! Resources: 0 added, 0 changed, 0 destroyed. +Outputs: +node_svc_public_ip = 34.71.90.74 +---- + +Couple of things to notice here. +First, we did not destroy anything, so terraform refreshes - it confirms that configurations are still as specified. +During this Terraform run, no resources have been created or changed, which means that the actual state of our infrastructure already meets the requirements of a desired state. + +Secondly, under "Outputs:", you should see the public IP of the VM we created. + +== Save and commit the work + +Save and commit the `05-terraform` folder created in this lab into your `iac-tutorial` repo. + +== Conclusion + +In this lab, you saw a state of the art the application of Infrastructure as Code practice. + +We used _code_ (Terraform configuration syntax) to describe the _desired state_ of the infrastructure. +Then we told Terraform to bring the actual state of the infrastructure to the desired state we described. + +With this approach, Terraform configuration becomes _a single source of truth_ about the current state of your infrastructure. +Moreover, the infrastructure is described as code, so we can apply to it the same practices we commonly use in development such as keeping the code in source control, use peer reviews for making changes, etc. + +All of this helps us get control over even the most complex infrastructure. + +Destroy the resources created by Terraform and move on to the next lab. + +[source,bash] +---- +$ terraform destroy -auto-approve +---- + +Next: xref:06-ansible.adoc[Ansible] diff --git a/docs/06-ansible.adoc b/docs/06-ansible.adoc new file mode 100644 index 0000000..1564552 --- /dev/null +++ b/docs/06-ansible.adoc @@ -0,0 +1,300 @@ += Ansible + +In the previous lab, you used Terraform to implement Infrastructure as Code approach to managing the cloud infrastructure resources. +There is another major type of tooling we need to consider, and that is *Configuration Management* (CM) tools. + +When talking about CM tools, we can often meet the acronym `CAPS` which stands for Chef, Ansible, Puppet and Saltstack - the most known and commonly used CM tools. +In this lab, we're going to look at Ansible and see how CM tools can help us improve our operations. + +== Intro + +If you think about our current operations and what else there is to improve, you will probably see the potential problem in the deployment process. + +The way we do deployment right now is by connecting via SSH to a VM and running a deployment script. +And the problem here is not the connecting via SSH part, but running a script. + +_Scripts are bad at long term management of system configuration, because they make common system configuration operations complex and error-prone._ + +When you write a script, you use a scripting language syntax (Bash, Python) to write commands which you think should change the system's configuration. +And the problem is that there are too many ways people can write the code that is meant to do the same things, which is the reason why scripts are often difficult to read and understand. +Besides, there are various choices as to what language to use for a script: should you write it in Ruby which your colleagues know very well or Bash which you know better? + +Common configuration management operations are well-known: copy a file to a remote machine, create a folder, start/stop/enable a process, install packages, etc. +So _we need a tool that would implement these common operations in a well-known and tested way, providing us with a clean and understandable syntax for using them_. +This way we wouldn't have to write complex scripts ourselves each time for the same tasks, possibly making mistakes along the way, but instead just tell the tool what should be done: what packages should be present, what processes should be started, etc. + +This is exactly what CM tools do. +So let's check it out using Ansible as an example. + +== Install Ansible + +NOTE: this lab assumes Ansible v2.4 is installed. +It may not work as expected with other versions as things change quickly. + +Issue the following commands in the Google cloud shell (note that Ansible will not remain installed when your shell goes to sleep): + +[source,bash] +---- +$ sudo apt update +$ sudo apt install software-properties-common +$ sudo apt-add-repository --yes --update ppa:ansible/ansible +$ sudo apt install -y ansible +---- + +If you have issues, reference the instructions on how to install Ansible on your system from http://docs.ansible.com/ansible/latest/intro_installation.html[official documentation]. + +Verify that Ansible was installed by checking the version: + +[source,bash] +---- +$ ansible --version +---- + +== Infrastructure as Code project + +Create a new directory called `06-ansible` inside your `iac-tutorial` repo, which we'll use to save the work done in this lab. + +== Provision compute resources + +Start a VM and create other GCP resources for running your application applying Terraform configuration you wrote in the previous lab (destroy first if you have some still running): + +[source,bash] +---- +$ cd ./05-terraform # adapt this command as necessary to get to the directory +$ terraform apply -auto-approve +---- + +== Deploy playbook + +We'll rewrite our Bash script used for deployment using Ansible syntax. + +Ansible uses *tasks* to define commands used for system configuration. +Each Ansible task basically corresponds to one command in our Bash script. + +Each task uses some *module* to perform a certain operation on the configured system. +Modules are well tested functions which are meant to perform common system configuration operations. + +Let's look at our `install.sh` first to see what modules we might need to use: + +[source,bash] +---- +#!/bin/bash +set -e # exit immediately if anything returns non-zero. See https://www.javatpoint.com/linux-set-command + + +echo " ----- download, initialize, and run app ----- " +git clone https://github.com/dm-academy/node-svc-v1 +cd node-svc-v1 +git checkout 02 +npm install +npm install express +---- + +We clearly see here several types of operations: cloning a git repo and setting the branch, initializing npm, and installing express (a Node package). + +We also, to start the service, need to run this command: + +`$ sudo nodejs /home/node-user/node-svc-v1/server.js &` + +So we'll search for Ansible modules that allow to perform these operations. +Luckily, there are modules for all of these operations. + +Ansible uses YAML syntax to define tasks, which makes the configuration readable. + +Let's create a file called `deploy.yml` ("deploy" including both installation and launching) inside the `ansible` directory: + +[source,yaml] +---- +--- +- name: Deploy node-svc App + hosts: node-svc + tasks: + - name: Fetch the latest version of application code + # see https://docs.ansible.com/ansible/latest/modules/git_module.html + git: + repo: 'https://github.com/dm-academy/node-svc-v1' + dest: /home/node-user/node-svc-1 + version: "02" + register: clone + + - name: NPM install express and initialize app + # see https://docs.ansible.com/ansible/latest/modules/npm_module.html + npm: + name: express + global: yes + + - name: Install packages based on package.json. + npm: + path: /home/node-user/node-svc-1 + + - name: Start the nodejs server + # see https://codelike.pro/deploy-nodejs-app-with-ansible-git-pm2/ + sudo_user: node-user + command: pm2 start server.js --name node-app chdir=/home/node-user/node-svc-1s + ignore_errors: yes + when: npm_finished.changed +---- + +In this configuration file, which is called a *playbook* in Ansible terminology, we define several tasks. + +The `name` that precedes each task is used as a comment that will show up in the terminal when the task starts to run. + +`register` option allows to capture the result output from running a task. + +The `first task` uses git module to pull the code from GitHub. + +[source,yaml] +---- +- name: Fetch the latest version of application code + # see https://docs.ansible.com/ansible/latest/modules/git_module.html + git: + repo: 'https://github.com/dm-academy/node-svc-v1' + dest: /home/node-user/node-svc-1 + version: 02 + register: git_finished +---- + +The second task installs the npm package express and initializes the app in the specified directory: + +[source,yaml] +---- + + - name: NPM install express and initialize app + # see https://docs.ansible.com/ansible/latest/modules/npm_module.html + npm: + name: coffee-script + global: yes + + - name: Install packages based on package.json. + npm: + path: /home/node-user/node-svc-1 +---- + +The third task runs the server: + +[source,yaml] +---- + - name: Start the nodejs server + # see https://codelike.pro/deploy-nodejs-app-with-ansible-git-pm2/ + sudo_user: node-user + command: pm2 start server.js --name node-app chdir=/home/node-user/node-svc-1s + ignore_errors: yes + when: npm_finished.changed +---- + +Note, how for each module we use a different set of module options. +You can find full information about the options in a module's documentation. + +In the second task, we use a conditional statement http://docs.ansible.com/ansible/latest/playbooks_conditionals.html#the-when-statement[when] to make sure the `npm install` task is only run when the local repo was updated, i.e. +the output from running git clone command was changed. +This allows us to save some time spent on system configuration by not running unnecessary commands. + +On the same level as tasks, we also define a *handlers* block. +Handlers are special tasks which are run only in response to notification events from other tasks. +In our case, `node-svc` service gets restarted only when the `npm install` task is run. + +== Inventory file + +The way that Ansible works is simple: it connects to a remote VM (usually via SSH) and runs the commands that stand behind each module you used in your playbook. + +To be able to connect to a remote VM, Ansible needs information like IP address and credentials. +This information is defined in a special file called http://docs.ansible.com/ansible/latest/intro_inventory.html[inventory]. + +Create a file called `hosts.yml` inside `ansible` directory with the following content (make sure to change the `ansible_host` parameter to public IP of your VM): + +[source,yaml] +---- +node-svc: + hosts: + node-svc-01: + ansible_host: 35.35.35.35 + ansible_user: node-user +---- + +Here we define a group of hosts (`node-svc`) under which we list the hosts that belong to this group. +In this case, we list only one host under the hosts group and give it a name (`node-svc-01`) and information on how to connect to the host. + +Now note, that inside our `deploy.yml` playbook we specified `node-svc` host group in the `hosts` option before the tasks: + +[source,yaml] +---- +--- +- name: Deploy node-svc app + hosts: node-svc-01 + tasks: + ... +---- + +This will tell Ansible to run the following tasks on the hosts defined in hosts group `raddit-app`. + +== Ansible configuration + +Before we can run a deployment, we need to make some configuration changes to how Ansible views and manages our `ansible` directory. + +Let's define custom Ansible configuration for our directory. +Create a file called `ansible.cfg` inside the `ansible` directory with the following content: + +[source,ini] +---- +[defaults] +inventory = ./hosts.yml +private_key_file = ~/.ssh/node-user +host_key_checking = False +---- + +This custom configuration will tell Ansible what inventory file to use, what private key file to use for SSH connection and to skip the host checking key procedure. + +== Run playbook + +Now it's time to run your playbook and see how it works. + +Use the following commands to start a deployment: + +[source,bash] +---- +$ cd ./06-ansible +$ ansible-playbook deploy.yml +---- + +== Access Application + +Access the application in your browser by its public IP (don't forget to specify the port 3000) and make sure application has been deployed and is functional. + +== Futher Learning Ansible + +There's a whole lot to learn about Ansible. +Try playing around with it more and create a `playbook` which provides the same system configuration as your `configuration.sh` script. +Save it under the name `configuration.yml` inside the `ansible` folder, then use it inside https://www.packer.io/docs/provisioners/ansible.html[ansible provisioner] instead of shell in your Packer template. + +You can find an example of `configuration.yml` playbook https://github.com/Artemmkin/infrastructure-as-code-example/blob/master/ansible/configuration.yml[here]. + +And https://github.com/Artemmkin/infrastructure-as-code-example/blob/master/packer/raddit-base-image-ansible.json[here] is an example of a Packer template which uses ansible provisioner. + +== Save and commit the work + +Save and commit the `ansible` folder created in this lab into your `iac-tutorial` repo. + +== Idempotence + +One more advantage of CM tools over scripts is that commands they implement designed to be *idempotent* by default. + +Idempotence in this case means that even if you apply the same configuration changes multiple times the result will stay the same. + +This is important because some commands that you use in scripts may not produce the same results when run more than once. +So we always want to achieve idempotence for our configuration management system, sometimes applying conditionals statements as we did in this lab. + +== Conclusion + +Ansible provided us with a clean YAML syntax for performing common system configuration tasks. +This allowed us to get rid of our own implementation of configuration commands. + +It might not seem like a big improvement at this scale, because our deploy script is small, but it definitely brings order to system configuration management and is more noticeable at medium and large scale. + +Destroy the resources created by Terraform. + +[source,bash] +---- +$ terraform destroy +---- + +Next: xref:07-vagrant.adoc[Vagrant] diff --git a/docs/07-vagrant.adoc b/docs/07-vagrant.adoc new file mode 100644 index 0000000..7a58c48 --- /dev/null +++ b/docs/07-vagrant.adoc @@ -0,0 +1,309 @@ +== Vagrant (OPTIONAL, for local laptops/workstations only) + +In this lab, we're going to learn about https://www.vagrantup.com/[Vagrant] which is another tool that implements IaC approach and is often used for creating development environments. + +_This lab is optional for those of you want to learn Vagrant on your personal laptops and workstations. +It will not work on GCE virtual machines, university workstations, or Google Cloud Shell. +The alternative to local development is to create a local development VM in the cloud. +You may skip to xref:08-docker.adoc[Docker]_. + +== Intro + +Before this lab, our main focus was on how to create and manage an environment where our application runs and is accessible to the public. +Let's call that environment `production` for the sake of simplicity of referring to that later. + +But what is about our local environment where we develop the code? +Are there any problems with that? + +Running our application locally would require us installing all of its dependencies and configuring the local system pretty much the same way as we did in the previous labs. + +There are a few reasons why you don't want to do that: + +* `This can break your system`. +When you change your system configuration there are lot of things that can go wrong. +For example, when installing/removing different packages you can easily mess up the work of your system's package manager. +* `When something breaks in your system configuration, it can take a long time to fix`. +If you've messed up with you local system configuration, you either need to debug or reinstall your OS. +Both of these can take a lot of your time and should be avoided. +* `You have no idea what is your development environment actually looks like`. +Your local OS will certainly have its own specific configuration and packages installed, because you use it for every day tasks different than just running your application. +For this reason, even if your application works on your local machine, you cannot describe exactly what is required for it to run. +This is commonly known as the `works on my machine` problem and is often one of the reasons for a conflict between Dev and Ops. + +Based on these problems, let's draw some requirements for our local dev environment: + +* `We should know exactly what is inside.` This is important, so that we could properly configure other environments for running the application. +* `Isolation from our local system.` This leaves us with choices of a local/remote VM or containers. +* `Ability to quickly and easily recreate when it breaks.` + +Vagrant is a tool that allows to meet all of these requirements. +Let's find out how. + +== Install Vagrant and VirtualBox + +NOTE: this lab assumes Vagrant `v2.0.1` is installed. +It may not work as expected on other versions. + +https://www.vagrantup.com/downloads.html[Download] and install Vagrant on your system. + +Verify that Vagrant was successfully installed by checking the version: + +[source,bash] +---- +$ vagrant -v +---- + +https://www.virtualbox.org/wiki/Downloads[Download] and install VirtualBox for running virtual machines locally. + +Also, make sure virtualization feature is enabled for your CPU. +You would need to check BIOS settings for this. + +== Create a Vagrantfile + +If we compare Vagrant to the previous tools we've already learned, it reminds Terraform. +Like Terraform, Vagrant allows you to declaratively describe VMs you want to provision, but it focuses on managing VMs (and containers) exclusively, so it's no good for things like firewall rules or VPC networks in the cloud. + +To start a local VM using Vagrant, we need to define its characteristics in a special file called `Vagrantfile`. + +Create a file named `Vagrantfile` inside `iac-tutorial` directory with the following content: + +[source,ruby] +---- +Vagrant.configure("2") do |config| + # define provider configuration + config.vm.provider :virtualbox do |v| + v.memory = 1024 + end + # define a VM machine configuration + config.vm.define "raddit-app" do |app| + app.vm.box = "ubuntu/xenial64" + app.vm.hostname = "raddit-app" + end +end +---- + +Vagrant, like Terraform, doesn't start VMs itself. +It uses a `provider` component to communicate the instructions to the actual provider of infrastructure resources. + +In this case, we redefine Vagrant's default provider (VirtualBox) configuration to allocate 1024 MB of memory to each VM defined in this Vagrantfile: + +[source,ruby] +---- +# define provider configuration +config.vm.provider :virtualbox do |v| + v.memory = 1024 +end +---- + +We also specify characteristics of a VM we want to launch: what machine image (`box`) to use (Vagrant downloads a box from https://www.vagrantup.com/docs/vagrant-cloud/boxes/catalog.html[Vagrant Cloud]), and what hostname to assign to a started VM: + +[source,ruby] +---- +# define a VM machine configuration +config.vm.define "raddit-app" do |app| + app.vm.box = "ubuntu/xenial64" + app.vm.hostname = "raddit-app" +end +---- + +== Start a Local VM + +With the Vagrantfile created, you can start a VM on your local machine using Ubuntu 16.04 image from Vagrant Cloud. + +Run the following command inside the folder with your Vagrantfile: + +[source,bash] +---- +$ vagrant up +---- + +Check the current status of the VM: + +[source,bash] +---- +$ vagrant status +---- + +You can connect to a started VM via SSH using the following command: + +[source,bash] +---- +$ vagrant ssh +---- + +== Configure Dev Environment + +Now that you have a VM running on your local machine, you need to configure it to run your application: install ruby, mongodb, etc. + +There are many ways you can do that, which are known to you by now. +You can configure the environment manually, using scripts or some CM tool like Ansible. + +_It's best to use the same configuration and the same CM tools across all of your environments._ + +As we've already discussed, your application may work in your local environment, but it may not work on a remote VM running in production environment, because of the differences in configuration. +But when your configuration is the same across all of your environments, the application will not fail for reasons like a missing package and the system configuration can generally be excluded as a potential cause of a failure when it occurs. + +Because we chose to use Ansible for configuring our production environment in the previous lab, let's use it for configuration management of our dev environment, too. + +Change your Vagrantfile to look like this: + +[source,ruby] +---- +Vagrant.configure("2") do |config| + # define provider configuration + config.vm.provider :virtualbox do |v| + v.memory = 1024 + end + # define a VM configuration + config.vm.define "raddit-app" do |app| + app.vm.box = "ubuntu/xenial64" + app.vm.hostname = "raddit-app" + # sync a local folder with application code to the VM folder + app.vm.synced_folder "raddit-app/", "/srv/raddit-app" + # use port forwarding make application accessible on localhost + app.vm.network "forwarded_port", guest: 9292, host: 9292 + # system configuration is done by Ansible + app.vm.provision "ansible" do |ansible| + ansible.playbook = "ansible/configuration.yml" + end + end +end +---- + +We added Ansible provisioning to the Vagrantfile which allows us to run a playbook for system configuration. + +[source,ruby] +---- +# system configuration is done by Ansible +app.vm.provision "ansible" do |ansible| + ansible.playbook = "ansible/configuration.yml" +end +---- + +In the previous lab, it was given to you as a task to create a `configuration.yml` playbook that provides the same functionality as `configuration.sh` script we had used before. +If you did not do that, you can copy the playbook from https://github.com/Artemmkin/infrastructure-as-code-example/blob/master/ansible/configuration.yml[here] (place it inside `ansible` directory). +If you did create your own playbook, make sure you have a `pre_tasks` section as in https://github.com/Artemmkin/infrastructure-as-code-example/blob/master/ansible/configuration.yml[this example]. + +Note, that we also added a port forwarding rule for accessing our application and instructed Vagrant to sync a local folder with application code to a specified VM folder (`/srv/raddit-app`): + +[source,ruby] +---- +# sync a local folder with application code to the VM folder +app.vm.synced_folder "raddit-app/", "/srv/raddit-app" +# use port forwarding make application accessible on localhost +app.vm.network "forwarded_port", guest: 9292, host: 9292 +---- + +Now run the following command to configure the local dev environment: + +[source,bash] +---- +$ vagrant provision +---- + +Verify the configuration: + +[source,bash] +---- +$ vagrant ssh +$ ruby -v +$ bundle version +$ sudo systemctl status mongod +---- + +== Run Application Locally + +As we mentioned, we gave Vagrant the instruction to sync our folder with application to a VM's folder under the specified path. +This way we can develop the application on our host machine using our favorite code editor and then run that code inside the VM. + +We need to first reload a VM for chages in our Vagrantfile to take effect: + +[source,bash] +---- +$ vagrant reload +---- + +Then connect to the VM to start application: + +[source,bash] +---- +$ vagrant ssh +$ cd /srv/raddit-app +$ sudo bundle install +$ puma +---- + +The application should be accessible to you now at the following URL: http://localhost:9292 + +Stop the application using `ctrl + C` keys. + +== Mess Up Dev Environment + +One of our requirements to local dev environment was that you can freely mess it up and recreate in no time. + +Let's try that. + +Delete Ruby on the VM: + +[source,bash] +---- +$ vagrant ssh +$ sudo apt-get -y purge ruby +$ ruby -v +---- + +Try to run your application again (it should fail): + +[source,bash] +---- +$ cd /srv/raddit-app +$ puma +---- + +== Recreate Dev Environment + +Let's try to recreate our dev environment from scratch to see how big of a problem it will be. + +Run the following commands to destroy the current dev environment and create a new one: + +[source,bash] +---- +$ vagrant destroy -f +$ vagrant up +---- + +Once a new VM is up and running, try to launch your app in it: + +[source,bash] +---- +$ vagrant ssh +$ ruby -v +$ cd /srv/raddit-app +$ sudo bundle install +$ puma +---- + +The Ruby package should be present and the application should run without problems. + +Recreating a new dev environment was easy, took very little time and it didn't affect our host OS. +That's exactly what we needed. + +== Save and commit the work + +Save and commit the Vagrantfile created in this lab into your `iac-tutorial` repo. + +== Conclusion + +Vagrant was able to meet our requirements for dev environments. +It makes creating/recreating and configuring a dev environment easy and safe for our host operating system. + +Because we describe our local infrastructure in code in a Vagrantfile, we keep it in source control and make sure all our other colleagues have the same environment for the application as we do. + +Destroy the VM: + +[source,bash] +---- +$ vagrant destroy -f +---- + +Next: xref:08-docker.adoc[Docker] diff --git a/docs/08-docker.adoc b/docs/08-docker.adoc new file mode 100644 index 0000000..dd49135 --- /dev/null +++ b/docs/08-docker.adoc @@ -0,0 +1,189 @@ +== Docker + +In this lab, we will talk about managing containers for the first time in this tutorial. +Particularly, we will talk about https://www.docker.com/what-docker[Docker] which is the most widely used platform for running containers. + +== Intro + +Remember when we talked about packer, we mentioned a few words about `Immutable Infrastructure` model? +The idea was to package all application dependencies and application itself inside a machine image, so that we don't have to configure the system after start. +Containers implement the same model, but they do it in a more efficient way. + +Containers allow you to create self-contained isolated environments for running your applications. + +They have some significant advantages over VMs in terms of implementing Immutable Infrastructure model: + +* `Containers are much faster to start than VMs.` Container starts in seconds, while a VM takes minutes. +It's important when you're doing an update/rollback or scaling your service. +* `Containers enable better utilization of compute resources.` Very often computer resources of a VM running an application are underutilized. +Launching multiple instances of the same application on one VM has a lot of difficulties: different application versions may need different versions of dependent libraries, init scripts require special configuration. +With containers, running multiple instances of the same application on the same machine is easy and doesn't require any system configuration. +* `Containers are more lightweight than VMs.` Container images are much smaller than machine images, because they don't need a full operating system in order to run. +In fact, a container image can include just a single binary and take just a few MBs of your disk space. +This means that we need less space for storing the images and the process of distributing images goes faster. + +Let's try to implement `Immutable Infrastructure` model with Docker containers, while paying special attention to the `Dockerfile` part as a way to practice `Infrastructure as Code` approach. + +== (FOR PERSONAL LAPTOPS AND WORKSTATIONS ONLY) Install Docker Engine + +_Docker is already installed on Google Cloud Shell._ + +The https://docs.docker.com/engine/docker-overview/#docker-engine[Docker Engine] is the daemon that gets installed on the system and allows you to manage containers with simple CLI. + +https://www.docker.com/community-edition[Install] free Community Edition of Docker Engine on your system. + +Verify that the version of Docker Engine is \=> 17.09.0: + +[source,bash] +---- +$ docker -v +---- + +== (FOR ALL) Create Dockerfile + +You describe a container image that you want to create in a special file called *Dockerfile*. + +Dockerfile contains `instructions` on how the image should be built. +Here are some of the most common instructions that you can meet in a Dockerfile: + +* `FROM` is used to specify a `base image` for this build. +It's similar to the builder configuration which we defined in a Packer template, but in this case instead of describing characteristics of a VM, we simply specify a name of a container image used for build. +This should be the first instruction in the Dockerfile. +* `ADD` and `COPY` are used to copy a file/directory to the container. +See the https://stackoverflow.com/questions/24958140/what-is-the-difference-between-the-copy-and-add-commands-in-a-dockerfile[difference] between the two. +* `RUN` is used to run a command inside the image. +Mostly used for installing packages. +* `ENV` sets an environment variable available within the container. +* `WORKDIR` changes the working directory of the container to a specified path. +It basically works like a `cd` command on Linux. +* `CMD` sets a default command, which will be executed when a container starts. +This should be a command to start your application. + +Let's use these instructions to create a Docker container image for our node-svc application. + +Inside your `my-iac-tutorial` directory, create a directory called `08-docker`, and in it a text file called `Dockerfile` with the following content: + +---- +FROM node:11 +# Create app directory +WORKDIR /app +# Install app dependencies +# A wildcard is used to ensure both package.json AND package-lock.json are copied +# where available (npm@5+) +COPY package*.json ./ +RUN npm install +RUN npm install express +# If you are building your code for production +# RUN npm ci --only=production +# Bundle app source +COPY . /app +EXPOSE 3000 +CMD [ "node", "server.js" ] +---- + +This Dockerfile repeats the steps that we did multiple times by now to configure a running environment for our application and run it. + +We first choose an image that already contains Node of required version: + +---- +# Use base image with node installed +FROM node:11 +---- + +The base image is downloaded from Docker official registry (storage of images) called https://hub.docker.com/[Docker Hub]. + +We then install required system packages and application dependencies: + +---- +# Install app dependencies +# A wildcard is used to ensure both package.json AND package-lock.json are copied +# where available (npm@5+)COPY package*.json ./ +RUN npm install +RUN npm install express +---- + +Then we copy the application itself. + +---- +# create application home directory and copy files +COPY . /app +---- + +Then we specify a default command that should be run when a container from this image starts: + +---- +CMD [ "node", "server.js" ] +---- + +== Build Container Image + +Once you defined how your image should be built, run the following command inside your `my-iac-tutorial` directory to create a container image for the node-svc application: + +[source,bash] +---- +$ docker build -t /node-svc-v1 . +---- + +The resulting image will be named `node-svc`. +Find it in the list of your local images: + +[source,bash] +---- +$ docker images | grep node-svc +---- + +At your option, you can save your build command in a script, such as `build.sh`. + +Now, run the container: + +[source,bash] +---- +$ docker run -d -p 8081:3000 /node-svc-v1 +---- + +Notice the "8081:3000" syntax. +This means that while the container is running on port 3000 internally, it is externally exposed via port 8081. + +Again, you may wish to save this in a script, such as `run.sh`. + +Now, test the container: + +[source,bash] +---- +$ curl localhost:8081 +Successful request. +---- + +Again, you may wish to save this in a script, such as `test.sh`. + +== Save and commit the work + +Save and commit the files created in this lab. + +== Conclusion + +In this lab, you adopted containers for running your application. +This is a different type of technology from what we used to deal with in the previous labs. +Nevertheless, we use Infrastructure as Code approach here, too. + +We describe the configuration of our container image in a Dockerfile using Dockerfile's syntax. +We then save that Dockefile in our application repository. +This way we can build the application image consistently across any environments. + +Destroy the current playground before moving on to the next lab, through `docker ps`, `docker kill`, `docker images`, and `docker rmi`. +In the example below, the container is named "beautiful_pascal". +Yours will be different. +Follow the example, substituting yours. + +[source,bash] +---- +$ docker ps +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +64e60b7b0c81 charlestbetz/node-svc-v1 "docker-entrypoint.s…" 10 minutes ago Up 10 minutes 0.0.0.0:8081->3000/tcp beautiful_pascal +$ docker kill beautiful_pascal +$ docker images +# returns list of your images +$ docker rmi -f +---- + +Next: xref:09-docker-compose.adoc[Docker Compose] diff --git a/docs/09-docker-compose.adoc b/docs/09-docker-compose.adoc new file mode 100644 index 0000000..bd0e7fe --- /dev/null +++ b/docs/09-docker-compose.adoc @@ -0,0 +1,178 @@ +== Docker Compose + +== PENDING CREATION OF node-svc MULTI-ARCHITECTURE + +== DISREGARD RADDIT APP + +In the last lab, we learned how to create Docker container images using Dockerfile and implementing Infrastructure as Code approach. + +This time we'll learn how to describe in code and manage our local container infrastructure with https://docs.docker.com/compose/overview/[Docker Compose]. + +== Intro + +Remember how in the previous lab we had to use a lot of `docker` CLI commands in order to run our application locally? +Specifically, we had to create a network for containers to communicate, a volume for container with MongoDB, launch MongoDB container, launch our application container. + +This is a lot of manual work and we only have 2 containers in our setup. +Imagine how much work it would be to run a microservices application which includes a dozen of services. + +To make the management of our local container infrastructure easier and more reliable, we need a tool that would allow us to describe the desired state of a local environment and then it would create it from our description. + +*Docker Compose* is exactly the tool we need. +Let's see how we can use it. + +== (FOR PERSONAL LAPTOPS AND WORKSTATIONS ONLY) Install Docker Compose + +Follow the official documentation on https://docs.docker.com/compose/install/[how to install Docker Compose] on your system. + +Verify that installed version of Docker Compose is \=> 1.18.0: + +[source,bash] +---- +$ docker-compose -v +---- + +== Describe Local Container Infrastructure + +Docker Compose could be compared to Terraform, but it manages only Docker container infrastructure. +It allows us to start containers, create networks and volumes, pass environment variables to containers, publish ports, etc. + +Let's use Docker Compose https://docs.docker.com/compose/compose-file/[declarative syntax] to describe what our local container infrastructure should look like. + +Create a file called `docker-compose.yml` inside your `iac-tutorial` repo with the following content: + +[source,yml] +---- +version: '3.3' + +# define services (containers) that should be running +services: + mongo-database: + image: mongo:3.2 + # what volumes to attach to this container + volumes: + - mongo-data:/data/db + # what networks to attach this container + networks: + - raddit-network + + raddit-app: + # path to Dockerfile to build an image and start a container + build: . + environment: + - DATABASE_HOST=mongo-database + ports: + - 9292:9292 + networks: + - raddit-network + # start raddit-app only after mongod-database service was started + depends_on: + - mongo-database + +# define volumes to be created +volumes: + mongo-data: +# define networks to be created +networks: + raddit-network: +---- + +In this compose file, we define 3 sections for configuring different components of our container infrastructure. + +Under the *services* section we define what containers we want to run. +We give each service a `name` and pass the options such as what `image` to use to launch container for this service, what `volumes` and `networks` should be attached to this container. + +If you look at `mongo-database` service definition, you should find it to be very similar to the docker command that we used to start MongoDB container in the previous lab: + +[source,bash] +---- +$ docker run --name mongo-database \ + --volume mongo-data:/data/db \ + --network raddit-network \ + --detach mongo:3.2 +---- + +So the syntax of Docker Compose can be easily understood by a person not even familiar with it https://docs.docker.com/compose/compose-file/#service-configuration-reference[the documentation]. + +`raddit-app` services configuration is a bit different from MongoDB service in a way that we specify a `build` option instead of `image` to build the container image from a Dockerfile before starting a container: + +[source,yml] +---- +raddit-app: + # path to Dockerfile to build an image and start a container + build: . + environment: + - DATABASE_HOST=mongo-database + ports: + - 9292:9292 + networks: + - raddit-network + # start raddit-app only after mongod-database service was started + depends_on: + - mongo-database +---- + +Also, note the `depends_on` option which allows us to tell Docker Compose that this `raddit-app` service depends on `mongo-database` service and should be started after `mongo-database` container was launched. + +The other two top-level sections in this file are *volumes* and *networks*. +They are used to define volumes and networks that should be created: + +[source,yml] +---- +# define volumes to be created +volumes: + mongo-data: +# define networks to be created +networks: + raddit-network: +---- + +These basically correspond to the commands that we used in the previous lab to create a named volume and a network: + +[source,bash] +---- +$ docker volume create mongo-data +$ docker network create raddit-network +---- + +== Create Local Infrastructure + +Once you described the desired state of you infrastructure in `docker-compose.yml` file, tell Docker Compose to create it using the following command: + +[source,bash] +---- +$ docker-compose up +---- + +or use this command to run containers in the background: + +[source,bash] +---- +$ docker-compose up -d +---- + +== Access Application + +The application should be accessible to your as before via the web preview icon in Google Cloud Shell. +`curl localhost:9292` will at least dump out the HTML (not very pretty, but if you see HTML you know the service is working to some degree at least). + +== Save and commit the work + +Save and commit the `docker-compose.yml` file created in this lab into your `iac-tutorial` repo. + +== Conclusion + +In this lab, we learned how to use Docker Compose tool to implement Infrastructure as Code approach to managing a local container infrastructure. +This helped us automate and document the process of creating all the necessary components for running our containerized application. + +If we keep created `docker-compose.yml` file inside the application repository, any of our colleagues can create the same container environment on any system with just one command. +This makes Docker Compose a perfect tool for creating local dev environments and simple application deployments. + +To destroy the local playground, run the following command: + +[source,bash] +---- +$ docker-compose down --volumes +---- + +Next: xref:10-kubernetes.adoc[Kubernetes] diff --git a/docs/09-docker-compose.md b/docs/09-docker-compose.md index 0659125..7ce228d 100644 --- a/docs/09-docker-compose.md +++ b/docs/09-docker-compose.md @@ -1,5 +1,8 @@ ## Docker Compose +## PENDING CREATION OF node-svc MULTI-ARCHITECTURE +## DISREGARD RADDIT APP + In the last lab, we learned how to create Docker container images using Dockerfile and implementing Infrastructure as Code approach. This time we'll learn how to describe in code and manage our local container infrastructure with [Docker Compose](https://docs.docker.com/compose/overview/). diff --git a/docs/10-kubernetes.adoc b/docs/10-kubernetes.adoc new file mode 100644 index 0000000..268c7f3 --- /dev/null +++ b/docs/10-kubernetes.adoc @@ -0,0 +1,486 @@ +== Kubernetes + +In the previous labs, we learned how to run Docker containers locally. +Running containers at scale is quite different and a special class of tools, known as *orchestrators*, are used for that task. + +In this lab, we'll take a look at the most popular Open Source orchestration platform called https://kubernetes.io/[Kubernetes] and see how it implements Infrastructure as Code model. + +== Intro + +We used Docker Compose to consistently create container infrastructure on one machine (our local machine). +However, our production environment may include tens or hundreds of VMs to have enough capacity to provide service to a large number of users. +What do you do in that case? + +Running Docker Compose on each VM from the cluster seems like a lot of work. +Besides, if you want your containers running on different hosts to communicate with each other it requires creation of a special type of network called `overlay`, which you can't create using only Docker Compose. + +Moreover, questions arise as to: + +* how to load balance containerized applications? +* how to perform container health checks and ensure the required number of containers is running? + +The world of containers is very different from the world of virtual machines and needs a special platform for management. + +Kubernetes is the most widely used orchestration platform for running and managing containers at scale. +It solves the common problems (some of which we've mentioned above) related to running containers on multiple hosts. +And we'll see in this lab that it uses the Infrastructure as Code approach to managing container infrastructure. + +Let's try to run our `raddit` application on a Kubernetes cluster. + +== (FOR PERSONAL LAPTOPS AND WORKSTATIONS ONLY) Install Kubectl + +Kubectl is installed on the Google Cloud Shell. + +https://kubernetes.io/docs/reference/kubectl/overview/[Kubectl] is command line tool that we will use to run commands against the Kubernetes cluster. + +You can install `kubectl` onto your system as part of Google Cloud SDK by running the following command: + +[source,bash] +---- +$ gcloud components install kubectl +---- + +Check the version of kubectl to make sure it is installed: + +[source,bash] +---- +$ kubectl version +---- + +== Infrastructure as Code project + +Create a new directory called `kubernetes` inside your `iac-tutorial` repo, which we'll use to save the work done in this lab. + +== Describe Kubernetes cluster in Terraform + +We'll use https://cloud.google.com/kubernetes-engine/[Google Kubernetes Engine] (GKE) service to deploy a Kubernetes cluster of 3 nodes. + +We'll describe a Kubernetes cluster using Terraform so that we can manage it through code. + +Create a directory named `terraform` inside `kubernetes` directory. +Create three files within it: + +[source,bash] +---- +variables.tf +terraform.tfvars +main.tf +---- + +=== variables.tf + +[source,bash] +---- +# Provider configuration variables +variable "project_id" { + description = "Project ID in GCP" +} + +variable "region" { + description = "Region in which to manage GCP resources" +} + +# Cluster configuration variables +variable "cluster_name" { + description = "The name of the cluster, unique within the project and zone" +} + +variable "zone" { + description = "The zone in which nodes specified in initial_node_count should be created in" +} +---- + +=== terraform.tfvars + +[source,bash] +---- +// define provider configuration variables +project_id = "some-project-ID" # project in which to create a cluster +region = "some-google-region" # region in which to create a cluster + +// define Kubernetes cluster variables +cluster_name = "iac-tutorial-cluster" # cluster name +zone = "some-google-zone" # zone in which to create a cluster nodes +---- + +=== main.tf + +[source,bash] +---- +resource "google_container_cluster" "primary" { + name = "${var.cluster_name}" + location = "${var.zone}" + initial_node_count = 3 + + master_auth { + username = "" + password = "" + + client_certificate_config { + issue_client_certificate = false + } + } + + # configure kubectl to talk to the cluster + provisioner "local-exec" { + command = "gcloud container clusters get-credentials ${var.cluster_name} --zone ${var.zone} --project ${var.project_id}" + } + + node_config { + oauth_scopes = [ + "https://www.googleapis.com/auth/compute", + "https://www.googleapis.com/auth/devstorage.read_only", + "https://www.googleapis.com/auth/logging.write", + "https://www.googleapis.com/auth/monitoring", + ] + + metadata = { + disable-legacy-endpoints = "true" + } + + tags = ["iac-kubernetes"] + } + + timeouts { + create = "30m" + update = "40m" + } +} + +# create firewall rule to allow access to application +resource "google_compute_firewall" "nodeports" { + name = "node-port-range" + network = "default" + + allow { + protocol = "tcp" + ports = ["30000-32767"] + } + source_ranges = ["0.0.0.0/0"] +} +---- + +We'll use this Terraform code to create a Kubernetes cluster. + +== Create Kubernetes Cluster + +`main.tf` holds all the information about the cluster that should be created. +It's parameterized using Terraform https://www.terraform.io/intro/getting-started/variables.html[input variables] which allow you to easily change configuration parameters. + +Look into `terraform.tfvars` file which contains definitions of the input variables and change them if necessary. +You'll most probably want to change `project_id` value. + +---- +// define provider configuration variables +project_id = "infrastructure-as-code" # project in which to create a cluster +region = "europe-west1" # region in which to create a cluster + +// define Kubernetes cluster variables +cluster_name = "iac-tutorial-cluster" # cluster name +zone = "europe-west1-b" # zone in which to create a cluster nodes +---- + +After you've defined the variables, run Terraform inside `kubernetes/terraform` to create a Kubernetes cluster consisting of 2 nodes (VMs for running our application containers). + +[source,bash] +---- +$ gcloud services enable container.googleapis.com # enable Kubernetes Engine API +$ terraform init +$ terraform apply +---- + +Wait until Terraform finishes creation of the cluster. +It can take about 3-5 minutes. + +Check that the cluster is running and `kubectl` is properly configured to communicate with it by fetching cluster information: + +[source,bash] +---- +$ kubectl cluster-info + +Kubernetes master is running at https://35.200.56.100 +GLBCDefaultBackend is running at https://35.200.56.100/api/v1/namespaces/kube-system/services/default-http-backend/proxy +... +---- + +== Deployment manifest + +Kubernetes implements Infrastructure as Code approach to managing container infrastructure. +It uses special entities called *objects* to represent the `desired state` of your cluster. +With objects you can describe + +* What containerized applications are running (and on which nodes) +* The compute resources available to those applications +* The policies around how those applications behave, such as restart policies, upgrades, and fault-tolerance + +By creating an object, you're effectively telling the Kubernetes system what you want your cluster's workload to look like; +this is your cluster's `desired state`. +Kubernetes then makes sure that the cluster's actual state meets the desired state described in the object. + +Most of the times, you describe the object in a `.yaml` file called `manifest` and then give it to `kubectl` which in turn is responsible for relaying that information to Kubernetes via its API. + +*Deployment object* represents an application running on your cluster. +We'll use it to run containers of our applications. + +Create a directory called `manifests` inside `kubernetes` directory. +Create a `deployments.yaml` file inside it with the following content: + +[source,yaml] +---- +apiVersion: apps/v1beta1 # implies the use of kubernetes 1.7 + # use apps/v1beta2 for kubernetes 1.8 +kind: Deployment +metadata: + name: raddit-deployment +spec: + replicas: 2 + selector: + matchLabels: + app: raddit + template: + metadata: + labels: + app: raddit + spec: + containers: + - name: raddit + image: dmacademy/raddit + env: + - name: DATABASE_HOST + value: mongo-service +--- +apiVersion: apps/v1beta1 # implies the use of kubernetes 1.7 + # use apps/v1beta2 for kubernetes 1.8 +kind: Deployment +metadata: + name: mongo-deployment +spec: + replicas: 1 + selector: + matchLabels: + app: mongo + template: + metadata: + labels: + app: mongo + spec: + containers: + - name: mongo + image: mongo:3.2 +---- + +In this file we describe two `Deployment objects` which define what application containers and in what quantity should be run. +The Deployment objects have the same structure so I'll briefly go over only one of them. + +Each Kubernetes object has 4 required fields: + +* `apiVersion` - Which version of the Kubernetes API you're using to create this object. +You'll need to change that if you're using Kubernetes API version different than 1.7 as in this example. +* `kind` - What kind of object you want to create. +In this case we create a Deployment object. +* `metadata` - Data that helps uniquely identify the object. +In this example, we give the deployment object a name according to the name of an application it's used to run. +* `spec` - describes the `desired state` for the object. +`Spec` configuration will differ from object to object, because different objects are used for different purposes. + +In the Deployment object's spec we specify, how many `replicas` (instances of the same application) we want to run and what those applications are (`selector`) + +[source,yml] +---- +spec: + replicas: 2 + selector: + matchLabels: + app: raddit +---- + +In our case, we specify that we want to be running 2 instances of applications that have a label `app=raddit`. +*Labels* are used to give identifying attributes to Kubernetes objects and can be then used by *label selectors* for objects selection. + +We also specify a `Pod template` in the spec configuration. +*Pods* are lower level objects than Deployments and are used to run only `a single instance of application`. +In most cases, Pod is equal to a container, although you can run multiple containers in a single Pod. + +The `Pod template` which is a Pod object's definition nested inside the Deployment object. +It has the required object fields such as `metadata` and `spec`, but it doesn't have `apiVersion` and `kind` fields as those would be redundant in this case. +When we create a Deployment object, the Pod object(s) will be created as well. +The number of Pods will be equal to the number of `replicas` specified. +The Deployment object ensures that the right number of Pods (`replicas`) is always running. + +In the Pod object definition (`Pod template`) we specify container information such as a container image name, a container name, which is used by Kubernetes to run the application. +We also add labels to identify what application this Pod object is used to run, this label value is then used by the `selector` field in the Deployment object to select the right Pod object. + +[source,yaml] +---- + template: + metadata: + labels: + app: raddit + spec: + containers: + - name: raddit + image: dmacademy/raddit + env: + - name: DATABASE_HOST + value: mongo-service +---- + +Notice how we also pass an environment variable to the container. +`DATABASE_HOST` variable tells our application how to contact the database. +We define `mongo-service` as its value to specify the name of the Kubernetes service to contact (more about the Services will be in the next section). + +Container images will be downloaded from Docker Hub in this case: the generic mongo container and the raddit image uploaded to the dmacademy organization. + +_It would be nice if we could use the locally built raddit image. +Extra credit for anyone who can figure out how to do that._ + +== Create Deployment Objects + +Run a kubectl command to create Deployment objects inside your Kubernetes cluster (make sure to provide the correct path to the manifest file): + +[source,bash] +---- +$ kubectl apply -f manifests/deployments.yaml +---- + +Check the deployments and pods that have been created: + +[source,bash] +---- +$ kubectl get deploy +$ kubectl get pods +---- + +== Service manifests + +Running applications at scale means running _multiple containers spread across multiple VMs_. + +This arises questions such as: How do we load balance between all of these application containers? +How do we provide a single entry point for the application so that we could connect to it via that entry point instead of connecting to a particular container? + +These questions are addressed by the *Service* object in Kubernetes. +A Service is an abstraction which you can use to logically group containers (Pods) running in you cluster, that all provide the same functionality. + +When a Service object is created, it is assigned a unique IP address called `clusterIP` (a single entry point for our application). +Other Pods can then be configured to talk to the Service, and the Service will load balance the requests to containers (Pods) that are members of that Service. + +We'll create a Service for each of our applications, i.e. +`raddit` and `MondoDB`. +Create a file called `services.yaml` inside `kubernetes/manifests` directory with the following content: + +[source,yaml] +---- +apiVersion: v1 +kind: Service +metadata: + name: raddit-service +spec: + type: NodePort + selector: + app: raddit + ports: + - protocol: TCP + port: 9292 + targetPort: 9292 + nodePort: 30100 +--- +apiVersion: v1 +kind: Service +metadata: + name: mongo-service +spec: + type: ClusterIP + selector: + app: mongo + ports: + - protocol: TCP + port: 27017 + targetPort: 27017 +---- + +In this manifest, we describe 2 Service objects of different types. +You should be already familiar with the general object structure, so I'll just go over the `spec` field which defines the desired state of the object. + +The `raddit` Service has a NodePort type: + +[source,yaml] +---- +spec: + type: NodePort +---- + +This type of Service makes the Service accessible on each Node's IP at a static port (NodePort). +We use this type to be able to contact the `raddit` application later from outside the cluster. + +`selector` field is used to identify a set of Pods to which to route packets that the Service receives. +In this case, Pods that have a label `app=raddit` will become part of this Service. + +[source,yaml] +---- + selector: + app: raddit +---- + +The `ports` section specifies the port mapping between a Service and Pods that are part of this Service and also contains definition of a node port number (`nodePort`) which we will use to reach the Service from outside the cluster. + +[source,yaml] +---- + ports: + - protocol: TCP + port: 9292 + targetPort: 9292 + nodePort: 30100 +---- + +The requests that come to any of your cluster nodes' public IP addresses on the specified `nodePort` will be routed to the `raddit` Service cluster-internal IP address. +The Service, which is listening on port 9292 (`port`) and is accessible within the cluster on this port, will then route the packets to the `targetPort` on one of the Pods which is part of this Service. + +`mongo` Service is only different in its type. +`ClusterIP` type of Service will make the Service accessible on the cluster-internal IP, so you won't be able to reach it from outside the cluster. + +== Create Service Objects + +Run a kubectl command to create Service objects inside your Kubernetes cluster (make sure to provide the correct path to the manifest file): + +[source,bash] +---- +$ kubectl apply -f manifests/services.yaml +---- + +Check that the services have been created: + +[source,bash] +---- +$ kubectl get svc +---- + +== Access Application + +Because we used `NodePort` type of service for the `raddit` service, our application should accessible to us on the IP address of any of our cluster nodes. + +Get a list of IP addresses of your cluster nodes: + +[source,bash] +---- +$ gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances list --filter="tags.items=iac-kubernetes" +---- + +Use any of your nodes public IP addresses and the node port `30100` which we specified in the service object definition to reach the `raddit` application in your browser. + +== Save and commit the work + +Save and commit the `kubernetes` folder created in this lab into your `iac-tutorial` repo. + +== Conclusion + +In this lab, we learned about Kuberenetes - a popular orchestration platform which simplifies the process of running containers at scale. +We saw how it implements the Infrastructure as Code approach in the form of `objects` and `manifests` which allow you to describe in code the desired state of your container infrastructure which spans a cluster of VMs. + +To destroy the Kubernetes cluster, run the following command inside `kubernetes/terraform` directory: + +[source,bash] +---- +$ terraform destroy +---- + +Next: xref:50-what-is-iac.adoc[What is Infrastructure as Code] diff --git a/docs/50-what-is-iac.adoc b/docs/50-what-is-iac.adoc new file mode 100644 index 0000000..62e3bfe --- /dev/null +++ b/docs/50-what-is-iac.adoc @@ -0,0 +1,22 @@ += What is Infrastructure as Code? + +You've come a long way going through all the labs and learning about different Infrastructure as Code tools. +Some sort of presentation of what Infrastructure as Code is should already be shaped in your head. + +To conclude this tutorial, I summarize some of the key points about what Infrastructure as Code means. + +. `We use code to describe infrastructure`. +We don't use UI to launch a VM, we decribe its desired characteristics in code and tell the tool to do that. +. `Everyone is using the same tested code for infrastructure management operations and not creating its own implementation each time`. +We talked about it when discussing downsides of scripts. +Common infrastructure management operations should rely on tested code functions which are used in the team. +It makes everyday operations more time efficient and less error-prone. +. `Automated operations`. +We don't run commands ourselves to launch and configure a system, but instead use a configuration syntax provided by IaC tool to tell it what should be done. +. `We apply software development practices to infrastructure`. +In software development, practices like keeping code in source control or peer reviews are very common. +They make development reliable and working in a team possible. +Since our infrastructure is described in code, we can apply the same practices to our infrastructure work. + +These are the points that I would make for now. +If you feel like there is something else to add or change, please feel free to send a pull request :) diff --git a/docs/convert.sh b/docs/convert.sh new file mode 100755 index 0000000..d600e82 --- /dev/null +++ b/docs/convert.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +find ./ -name "*.md" \ + -type f | xargs -I @@ \ + bash -c 'kramdoc \ + --format=GFM \ + --wrap=ventilate \ + --output=./@@.adoc ./@@'; diff --git a/docs/convert2.sh b/docs/convert2.sh new file mode 100755 index 0000000..5d89e8d --- /dev/null +++ b/docs/convert2.sh @@ -0,0 +1,4 @@ +kramdoc --format=GFM \ + --output=./00-introduction.adoc \ + --wrap=ventilate \ + ./00-introduction.md From a9e3b9e8142d3693c8718aa9f59f000a8fdd5eca Mon Sep 17 00:00:00 2001 From: Charles Betz Date: Sat, 22 Aug 2020 11:04:43 -0500 Subject: [PATCH 93/93] removed md --- README.md | 49 ---- docs/00-introduction.md | 17 -- docs/01-prerequisites.md | 32 --- docs/02-manual-operations.md | 171 -------------- docs/03-scripts.md | 209 ----------------- docs/04-packer.md | 216 ------------------ docs/05-terraform.md | 306 ------------------------- docs/06-ansible.md | 267 ---------------------- docs/07-vagrant.md | 265 ---------------------- docs/08-docker.md | 157 ------------- docs/09-docker-compose.md | 159 ------------- docs/10-kubernetes.md | 419 ----------------------------------- docs/50-what-is-iac.md | 12 - 13 files changed, 2279 deletions(-) delete mode 100644 README.md delete mode 100644 docs/00-introduction.md delete mode 100644 docs/01-prerequisites.md delete mode 100644 docs/02-manual-operations.md delete mode 100644 docs/03-scripts.md delete mode 100644 docs/04-packer.md delete mode 100644 docs/05-terraform.md delete mode 100644 docs/06-ansible.md delete mode 100644 docs/07-vagrant.md delete mode 100644 docs/08-docker.md delete mode 100644 docs/09-docker-compose.md delete mode 100644 docs/10-kubernetes.md delete mode 100644 docs/50-what-is-iac.md diff --git a/README.md b/README.md deleted file mode 100644 index c68ac9d..0000000 --- a/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# Infrastructure As Code Tutorial - -[![license](https://img.shields.io/github/license/Artemmkin/infrastructure-as-code-tutorial.svg)](https://github.com/Artemmkin/infrastructure-as-code-tutorial/blob/master/LICENSE) -[![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Learn%20about%20Infrastructure%20as%20Code%20https%3A%2F%2Fgithub.com%2FArtemmkin%2Finfrastructure-as-code-tutorial%20%20Tutorial%20created%20by%20@artemmkins%20covers%20%23Packer,%20%23Terraform,%20%23Ansible,%20%23Vagrant,%20%23Docker,%20and%20%23Kubernetes.%20%23DevOps) - -(Intro by original author of this material) This tutorial is intended to show what the **Infrastructure as Code** (**IaC**) is, why we need it, and how it can help you manage your infrastructure more efficiently. - -It is practice-based, meaning I don't give much theory on what Infrastructure as Code is in the beginning of the tutorial, but instead let you feel it through the practice first. At the end of the tutorial, I summarize some of the key points about Infrastructure as Code based on what you learn through the labs. - -This tutorial is not meant to give a complete guide on how to use a specific tool like Ansible or Terraform, instead it focuses on how these tools work in general and what problems they solve. - -> The tutorial was inspired by [Kubernetes the Hard Way](https://github.com/kelseyhightower/kubernetes-the-hard-way) tutorial. I used it as an example to structure this one. - -_See [my presentation at DevOpsDays Silicon Valley](https://www.youtube.com/watch?v=XbcW2B7roLo&t=) in which I talk more in depth about the tutorial._ - -## Target Audience - -The target audience for this tutorial is anyone who loves or/and works in IT. - -## Tools Covered - -* Packer -* Terraform -* Ansible -* Vagrant -* Docker -* Docker Compose -* Kubernetes - -## Results of completing the tutorial - -By the end of this tutorial, you'll make your own repository looking like [this one](https://github.com/dm-academy/iac-tutorial-rsrc). Feel free to inspect it in case you get stuck in some of the labs. - -## Labs - -This tutorial assumes you have access to the Google Cloud Platform. While GCP is used for basic infrastructure requirements the lessons learned in this tutorial can be applied to other platforms. - -* [Introduction](docs/00-introduction.md) -* [Prerequisites](docs/01-prerequisites.md) -* [Manual Operations](docs/02-manual-operations.md) -* [Scripts](docs/03-scripts.md) -* [Packer](docs/04-packer.md) -* [Terraform](docs/05-terraform.md) -* [Ansible](docs/06-ansible.md) -* [Vagrant](docs/07-vagrant.md) -* [Docker](docs/08-docker.md) -* [Docker Compose](docs/09-docker-compose.md) -* [Kubernetes](docs/10-kubernetes.md) -* [What is Infrastructure as Code?](docs/50-what-is-iac.md) diff --git a/docs/00-introduction.md b/docs/00-introduction.md deleted file mode 100644 index 307ff9b..0000000 --- a/docs/00-introduction.md +++ /dev/null @@ -1,17 +0,0 @@ -# Introduction - -Let's dream for a little bit... - -Imagine that you're a young developer who developed a web application. You run and test your application locally and everything works great, which makes you very happy. You believe that this is going to blow the minds of Internet users and bring you a lot of money. - -Then you realize that there is a small problem. You ask yourself a question: "How do I make my application available to the Internet users?" - -You're thinking that you can't run the application locally all the time, because your old laptop will become slow for other tasks and will probably crash if a lot of users will be using your app at the same time. Besides, your ISP changes randomly the public IP for your router, so you don't know on which IP address your application will be accessible to the public at any given moment. - -You start realizing that the problem you're facing is not as small as you thought. In fact, there is a whole new craft for you to learn in IT world about running software applications and making sure they are always available to the users. - -The craft is called **IT operations**. And in almost every IT department, there is an operations (Ops) team who manages the platform where the applications are running. - -The tutorial you are about to begin will give you, a young developer, a bit of a glance into what operations work look like and how you can do this work more efficiently by using **Infrastructure as Code** approach. - -Next: [Prerequisites](01-prerequisites.md) diff --git a/docs/01-prerequisites.md b/docs/01-prerequisites.md deleted file mode 100644 index f5dd98e..0000000 --- a/docs/01-prerequisites.md +++ /dev/null @@ -1,32 +0,0 @@ -# Prerequisites - -## Google Cloud Platform - -In this tutorial, we use the [Google Cloud Platform](https://cloud.google.com/) to provision the compute infrastructure. You have already signed up. - -Start in the Google Cloud Shell. [(review)](https://cloud.google.com/shell/docs/using-cloud-shell) - -## Google Cloud Platform -### Set a Default Project, Compute Region and Zone - -This tutorial assumes a default compute region and zone have been configured. - -Set a default compute region appropriate to your location ([GCP regions and zones](https://cloud.google.com/compute/docs/regions-zones)): - -```bash -$ gcloud config set compute/region us-central1 -``` - -Set a default compute zone appropriate to the zone: - -```bash -$ gcloud config set compute/zone us-central1-c -``` - -Verify the configuration settings: - -```bash -$ gcloud config list -``` - -Next: [Manual operations](02-manual-operations.md) diff --git a/docs/02-manual-operations.md b/docs/02-manual-operations.md deleted file mode 100644 index 62c80d4..0000000 --- a/docs/02-manual-operations.md +++ /dev/null @@ -1,171 +0,0 @@ -# Manual Operations - -To better understand the `Infrastructure as Code` (`IaC`) concept, we will first define the problem we are facing and deal with it with manually to get our hands dirty and see how things work overall. - -## Intro - -Imagine you have developed a new cool application called [node-svc](https://github.com/dm-academy/node-svc-v1). - -You want to run your application on a dedicated server and make it available to the Internet users. - -You heard about the `public cloud` thing, which allows you to provision compute resources and pay only for what you use. You believe it's a great way to test your idea of an application and see if people like it. - -You've signed up for a free tier of [Google Cloud Platform](https://cloud.google.com/) (GCP) and are about to start deploying your application. - -## Provision Compute Resources - -First thing we will do is to provision a virtual machine (VM) inside GCP for running the application. - -Use the following gcloud command in your terminal to launch a VM with Ubuntu 16.04 distro: - -```bash -$ gcloud compute instances create node-svc\ - --image-family ubuntu-minimal-2004-lts \ - --image-project ubuntu-os-cloud \ - --boot-disk-size 10GB \ - --machine-type f1-micro -``` - -## Create an SSH key pair - -Generate an SSH key pair for future connections to the VM instances (run the command exactly as it is): - -```bash -$ ssh-keygen -t rsa -f ~/.ssh/node-user -C node-user -P "" -``` - -Create an SSH public key for your project: - -```bash -$ gcloud compute project-info add-metadata \ - --metadata ssh-keys="node-user:$(cat ~/.ssh/node-user.pub)" -``` - -Check your ssh-agent is running: - -```bash -$ echo $SSH_AGENT_PID -``` -If you get a number, it is running. If you get nothing, then run: - -```bash -$ eval `ssh-agent` -``` - -Add the SSH private key to the ssh-agent: - -``` -$ ssh-add ~/.ssh/node-user -``` - -Verify that the key was added to the ssh-agent: - -```bash -$ ssh-add -l -``` - -## Install Application Dependencies - -To start the application, you need to first configure the environment for running it. - -Connect to the started VM via SSH using the following two commands: - -```bash -$ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc) -$ ssh node-user@${INSTANCE_IP} -``` - -Install Node and npm: - -```bash -$ -$ sudo apt-get install -y nodejs npm -``` - -Check the installed version of Node: - -```bash -$ node -v -``` - -Install `git`: -```bash -$ sudo apt -y install git -``` - -Clone the application repo into the home directory of `node-user` user (reminder, how do you clone to the right location?): - -```bash -$ git clone https://github.com/dm-academy/node-svc-v1 -``` -Navigate to the repo (`cd node-svc-v1`) and check out the 02 branch (matching this lesson) - -```bash -$ git checkout 02 -Branch 02 set up to track remote branch 02 from origin. -Switched to a new branch '02' -``` - -Initialize npm (Node Package Manager) and install express: - -```bash -$ npm install -$ npm install express -``` - -## Start the Application - -Look at the server.js file (`cat`). We will discuss in class. - -Start the Node web server: - -```bash -$ nodejs server.js & -Running on 3000 -``` - -Test it: - -```bash -$ curl localhost:3000 -Successful request. -``` - - -## Access the Application - -Open a firewall port the application is listening on (note that the following command should be run on the Google Cloud Shell): - -```bash -$ gcloud compute firewall-rules create allow-node-svc-tcp-3000 \ - --network default \ - --action allow \ - --direction ingress \ - --rules tcp:3000 \ - --source-ranges 0.0.0.0/0 -``` - -Get the public IP of the VM: - -```bash -$ gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc-instance -``` - -Now open your browser and try to reach the application at the public IP and port 3000. - -For example, I put in my browser the following URL http://104.155.1.152:3000, but note that you'll have your own IP address. - -## Conclusion - -Congrats! You've just deployed your application. It is running on a dedicated set of compute resources in the cloud and is accessible by a public IP. Now Internet users can enjoy using your application. - -Now that you've got the idea of what sort of steps you have to take to deploy your code from your local machine to a virtual server running in the cloud, let's see how we can do it more efficiently. - -Destroy the current VM and firewall rule and move to the next step: - -```bash -$ gcloud compute instances delete -q node-svc -$ gcloud compute firewall-rules delete -q allow-node-svc-tcp-9292 -``` - -Next: [Scripts](03-scripts.md) diff --git a/docs/03-scripts.md b/docs/03-scripts.md deleted file mode 100644 index 89950e3..0000000 --- a/docs/03-scripts.md +++ /dev/null @@ -1,209 +0,0 @@ -# Scripts - -In the previous lab, you deployed the [node-svc](https://github.com/dm-academy/node-svc) application by connecting to a VM via SSH and running commands in the terminal one by one. In this lab, we'll try to automate this process a little by using `scripts`. - -## Intro - -Now think about what happens if your application becomes so popular that one virtual machine can't handle all the load of incoming requests. Or what happens when your application somehow crashes? Debugging a problem can take a long time and it would most likely be much faster to launch and configure a new VM than trying to fix what's broken. - -In all of these cases we face the task of provisioning new virtual machines, installing the required software and repeating all of the configurations we've made in the previous lab over and over again. - -Doing it manually is boring, error-prone and time-consuming. - -The most obvious way for improvement is using Bash scripts which allow us to run sets of commands put in a single file. So let's try this. - -## Infrastructure as Code project - -Starting from this lab, we're going to use a git repo for saving all the work done in this tutorial. - -Go to your Github account and create a new repository called iac-repo. No README or .gitignore. Copy the URL. - -Clone locally: - -```bash -$ git clone -``` - -Create a directory for this lab: - -```bash -$ cd iac-repo -$ mkdir 03-script -$ cd 03-script -``` - -To push your changes up to Github: - -```bash -$ git add . -A -$ git commit -m "first lab 03 commit" # should be relevant to the changes you made -$ git push origin master -``` - -Always issue these commands several times during each session. - -## Provisioning script - -We can automate the process of creating the VM and the firewall rule. - -In the `script` directory create a script `provision.sh`: - -```bash -#!/bin/bash -# add new VM -gcloud compute instances create node-svc \ - --image-family ubuntu-minimal-2004-lts \ - --image-project ubuntu-os-cloud \ - --boot-disk-size 10GB \ - --machine-type f1-micro - -# add firewall rule -gcloud compute firewall-rules create allow-node-svc-tcp-3000 \ - --network default \ - --action allow \ - --direction ingress \ - --rules tcp:3000 \ - --source-ranges 0.0.0.0/0 -``` - -Run it in the Google Cloud Shell: - -```bash -$ chmod +x provision.sh # changing permissions -$ ./provision.sh # you have to include the './' -``` - -You should see results similar to: - -```bash -WARNING: You have selected a disk size of under [200GB]. This may result in poor I/O performance. For more information, see: https://developers.google.com/compute/docs/disks#performance. -Created [https://www.googleapis.com/compute/v1/projects/proven-sum-252123/zones/us-central1-c/instances/node-svc]. -NAME ZONE MACHINE_TYPE PREEMPTIBLE INTERNAL_IP EXTERNAL_IP STATUS -node-svc us-central1-c n1-standard-1 10.128.15.202 34.69.206.6 RUNNING -Creating firewall...⠹Created [https://www.googleapis.com/compute/v1/projects/proven-sum-252123/global/firewalls/allow-node-svc-3000]. -Creating firewall...done. -NAME NETWORK DIRECTION PRIORITY ALLOW DENY DISABLED -allow-node-svc-3000 default INGRESS 1000 tcp:3000 False -``` - -## Installation script - -Before we can run our application, we need to create a running environment for it by installing dependent packages and configuring the OS. Then we copy the application, initialize NPM and download express.js, and start the server. - -We are going to use the same commands we used before to do that, but this time, instead of running commands one by one, we'll create a `bash script` to save us some struggle. - -In the `03-script` directory create bash script `config.sh` to install node, npm, express, and git. Create a script `install.sh` to download the app and initialize node. - -```bash -#!/bin/bash -set -e # exit immediately if anything returns non-zero. See https://www.javatpoint.com/linux-set-command - -echo " ----- install node, npm, git ----- " -apt-get update -apt-get install -y nodejs npm git -``` - -```bash -#!/bin/bash -set -e # exit immediately if anything returns non-zero. See https://www.javatpoint.com/linux-set-command - -echo " ----- download, initialize, and run app ----- " -git clone https://github.com/dm-academy/node-svc-v1 -cd node-svc-v1 -git checkout 02 -npm install -npm install express -``` - -NOTE: Why two scripts? Discuss in class. - - -## Run the scripts - -Copy the script to the created VM: - -```bash -$ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc) -$ scp -r config.sh install.sh node-user@${INSTANCE_IP}:/home/node-user -``` - -If sucessful, you should see something like: - -```bash -config.sh 100% 214 279.9KB/s 00:00 -install.sh 100% 214 279.9KB/s 00:00 -``` - -NOTE: If you get an `offending ECDSA key` error, use the suggested removal command. - -NOTE: If you get the error `Permission denied (publickey).`, this probably means that your ssh-agent no longer has the node-user private key added. This easily happens if the Google Cloud Shell goes to sleep and wipes out your session. Check via issuing `ssh-add -l`. - -If you get a message to the effect that your agent is not running, type ``eval `ssh-agent` `` and then `ssh-add -l`. - -You should see something like `2048 SHA256:bII5VsQY3fCWXEai0lUeChEYPaagMXun3nB9U2eoUEM /home/betz4871/.ssh/node-user (RSA)`. If you do not, re-issue the command `ssh-add ~/.ssh/node-user` and re-confirm with `ssh-add -l`. - - -Connect to the VM via SSH: -```bash -$ ssh node-user@${INSTANCE_IP} -``` -Have a look at what's in the directory (use `ls` and `cat`). Do you understand exactly how it got there? If you do not, ask. - -Run the script and launch the server: -```bash -$ chmod +x *.sh -$ sudo ./config.sh && ./install.sh # running 2 commands on one line -$ sudo nodejs node-svc-v1/server.js & -``` -The last output should be `Running on 3000`. You may need to hit Return or Enter to get a command prompt. - -To test that the server is running locally, type: -```bash -$ curl localhost:3000 -``` -You should receive this: - -```bash -Successful request. -``` -## Access the Application - -Access the application in your browser by its public IP (don't forget to specify the port 3000). - -Open another terminal and run the following command to get a public IP of the VM: - -```bash -$ gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc -``` -## Destroy (de-provision) the resources by script - -In the `provision` directory create a script `deprovision.sh`. - -```bash -#!/bin/bash -gcloud compute instances delete -q node-svc -gcloud compute firewall-rules delete -q allow-node-svc-tcp-3000 -``` - -Set permissions correctly (see previous) and execute. You should get results like: - -```bash -Deleted [https://www.googleapis.com/compute/v1/projects/proven-sum-252123/zones/us-central1-c/instances/node-svc]. -Deleted [https://www.googleapis.com/compute/v1/projects/proven-sum-252123/global/firewalls/allow-node-svc-tcp-3000].``` - -## Save and commit the work - -Save and commit the scripts created in this lab into your `iac-tutorial` repo. - -## Conclusion - -Scripts helped us to save some time and effort of manually running every command one by one to configure the system and start the application. - -The process of system configuration becomes more or less standardized and less error-prone, as you put commands in the order they should be run and test it to ensure it works as expected. - -It's also a first step we've made in the direction of automating operations work. - -But scripts are not suitable for every operations task and have many downsides. We'll discuss more on that in the next labs. - - -Next: [Packer](04-packer.md) diff --git a/docs/04-packer.md b/docs/04-packer.md deleted file mode 100644 index f926376..0000000 --- a/docs/04-packer.md +++ /dev/null @@ -1,216 +0,0 @@ -# Packer - -Scripts helped us speed up the process of system configuration, and made it more reliable compared to doing everything manually, but there are still ways for improvement. - -In this lab, we're going to take a look at the first IaC tool in this tutorial called [Packer](https://www.packer.io/) and see how it can help us improve our operations. - -## Intro - -Remember how in the second lab we had to make install nodejs, npm, and even git on the VM so that we could clone the application repo? Did it surprise you that `git` was not already installed on the system? - -Imagine how nice it would be to have required packages like nodejs and npm preinstalled on the VM we provision, or have necessary configuration files come with the image, too. This would require even less time and effort from us to configure the system and run our application. - -Luckily, we can create custom machine images with required configuration and software installed using Packer, an IaC tool by Hashicorp. Let's check it out. - -## Install Packer - -[Download](https://www.packer.io/downloads.html) and install Packer onto your system (this means the Google Cloud Shell). You will need to figure this out. - -If you have issues, consult [this script](https://github.com/dm-academy/iac-tutorial-rsrc/blob/master/packer/install-packer.sh). - -Check the version to verify that it was installed: - -```bash -$ packer -v -``` - -## Infrastructure as Code project - -Create a new directory called `04-packer` inside your `iac-tutorial` repo, which we'll use to save the work done in this lab. - -## Define image builder - -The way Packer works is simple. It starts a VM with specified characteristics, configures the operating system and installs the software you specify, and then it creates a machine image from that VM. - -The part of packer responsible for starting a VM and creating an image from it is called [builder](https://www.packer.io/docs/builders/index.html). - -So before using packer to create images, we need to define a builder configuration in a JSON file (which is called **template** in Packer terminology). - -Create a `node-svc-base-image.json` file inside the `packer` directory with the following content (make sure to change the project ID, and also the zone in case it's different): - -```json -{ - "builders": [ - { - "type": "googlecompute", - "project_id": "YOUR PROJECT HERE. YOU MUST CHANGE THIS", - "zone": "us-central1-c", - "machine_type": "f1-micro", - "source_image_family": "ubuntu-minimal-2004-lts", - "image_name": "node-svc-base-{{isotime \"2006-01-02 03:04:05\"}}", - "image_family": "node-svc-base", - "image_description": "Ubuntu 16.04 with git, nodejs, npm preinstalled", - "ssh_username": "node-user" - } - ] -} -``` - -This template describes where and what type of a VM to launch for image creation (`type`, `project_id`, `zone`, `machine_type`, `source_image_family`). It also defines image saving configuration such as under which name (`image_name`) and image family (`image_family`) the resulting image should be saved and what description to give it (`image_description`). SSH user configuration is used by provisioners which will talk about later. - -Validate the template: - -```bash -$ packer validate node-svc-base-image.json -``` - -## Define image provisioner - -As we already mentioned, builders are only responsible for starting a VM and creating an image from that VM. The real work of system configuration and installing software on the running VM is done by another Packer component called **provisioner**. - -Add a [shell provisioner](https://www.packer.io/docs/provisioners/shell.html) to your template to run the `deploy.sh` script you created in the previous lab. - -Your template should look similar to this one: - -```json -{ - "builders": [ - { - "type": "googlecompute", - "project_id": "YOUR PROJECT HERE. YOU MUST CHANGE THIS", - "zone": "us-central1-c", - "machine_type": "f1-micro", - "source_image_family": "ubuntu-1604-lts", - "image_name": "node-svc-base-{{isotime `20200901-000001`}}", - "image_family": "node-svc-base", - "image_description": "Ubuntu 16.04 with git, nodejs, npm, and node-svc preinstalled", - "ssh_username": "node-user" - } - ], - "provisioners": [ - { - "type": "shell", - "script": "{{template_dir}}/../03-script/config.sh", - "execute_command": "sudo {{.Path}}" - } - ] -} -``` - -Make sure the template is valid: - -```bash -$ packer validate ./packer/node-base-image.json -``` - -## Create custom machine image - -Build the image for your application: - -```bash -$ packer build node-svc-base-image.json -``` - -If you go to the [Compute Engine Images](https://console.cloud.google.com/compute/images) page you should see your new custom image. - -## Launch a VM with your custom built machine image - -Once the image is built, use it as a boot disk to start a VM: - -```bash -$ gcloud compute instances create node-svc \ - --image-family node-svc-base \ - --boot-disk-size 10GB \ - --machine-type f1-micro -``` - -## Deploy Application - -Copy the installation script to the VM: - -$ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc) -$ scp -r ../03-script/install.sh node-user@${INSTANCE_IP}:/home/node-user - -Connect to the VM via SSH: - -```bash -$ ssh node-user@${INSTANCE_IP} -``` - -NOTE: If you get an offending ECDSA key error, use the suggested removal command. - -NOTE: If you get the error `Permission denied (publickey).`, this probably means that your ssh-agent no longer has the node-user private key added. This easily happens if the Google Cloud Shell goes to sleep and wipes out your session. Check via issuing `ssh-add -l`. You should see something like `2048 SHA256:bII5VsQY3fCWXEai0lUeChEYPaagMXun3nB9U2eoUEM /home/betz4871/.ssh/node-user (RSA)`. If you do not, re-issue the command `ssh-add ~/.ssh/node-user` and re-confirm with `ssh-add -l`. - -Verify git, nodejs, and npmare installed. Do you understand how they got there? (Your results may be slightly different, but if you get errors, investigate or ask for help): - -```bash -node-user@node-svc:~$ npm -v -6.14.4 -node-user@node-svc:~$ node -v -v10.19.0 -node-user@node-svc:~$ git --version -git version 2.25.1 -``` - - -Run the installation script, and then the server: - -```bash -$ chmod +x *.sh -$ sudo ./install.sh -$ sudo nodejs node-svc-v1/server.js & -``` - -## Access Application - -Manually re-create the firewall rule: - -```bash -$ gcloud compute firewall-rules create allow-node-svc-tcp-3000 \ - --network default \ - --action allow \ - --direction ingress \ - --rules tcp:3000 \ - --source-ranges 0.0.0.0/0 -``` - -Open another terminal and run the following command to get a public IP of the VM: - -```bash -$ gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc -``` - -Access the application in your browser by its public IP (don't forget to specify the port 3000). - -## De-provision -```bash -$ ../03-script/deprovision.sh #notice path -``` - -## Save and commit the work - -Save and commit the packer template created in this lab into your `iac-tutorial` repo. - -## Learning more about Packer - -Packer configuration files are called templates for a reason. They often get parameterized with [user variables](https://www.packer.io/docs/templates/user-variables.html). This could be very helpful since you can create multiple machine images with different configurations for different purposes using one template file. - -Adding user variables to a template is easy, follow the [documentation](https://www.packer.io/docs/templates/user-variables.html) on how to do that. - -## Immutable infrastructure - -By putting everything inside the image including the application, we have achieved an [immutable infrastructure](https://martinfowler.com/bliki/ImmutableServer.html). It is based on the idea `we build it once, and we never change it`. - -It has advantages of spending less time (zero in this case) on system configuration after VM's start, and prevents **configuration drift**, but it's also not easy to implement. - -## Conclusion - -In this lab you've used Packer to create a custom machine image for running your application. - -Its advantages include: - -* `It requires less time and effort to configure a new VM for running the application` -* `System configuration becomes more reliable.` When we start a new VM to deploy the application, we know for sure that it has the right packages installed and configured properly, since we built and tested the image. - - -Next: [Terraform](05-terraform.md) diff --git a/docs/05-terraform.md b/docs/05-terraform.md deleted file mode 100644 index 7d4082f..0000000 --- a/docs/05-terraform.md +++ /dev/null @@ -1,306 +0,0 @@ -# Terraform - -In the previous lab, you used scripts to make your system configuration faster and more reliable. But we still have a lot to improve. - -In this lab, we're going to learn about the IaC tool by HashiCorp called [Terraform](https://www.terraform.io/). - -## Intro - -Think about your current operations... - -Do you see any problems you may have, or any ways for improvement? - -Remember, that each time we want to deploy an application, we have to `provision` compute resources first, that is to start a new VM. - -We do it via a `gcloud` command like this: - -```bash -$ gcloud compute instances create node-svc \ - --image-family ubuntu-minimal-2004-lts \ - --boot-disk-size 10GB \ - --machine-type f1-micro -``` - -At this stage, it doesn't seem like there are any problems with this. But, in fact, there are. - -Infrastructure for running your services and applications could be huge. You might have tens, hundreds or even thousands of virtual machines, hundreds of firewall rules, multiple VPC networks and load balancers. Additionally, the infrastructure could be split between multiple teams. Such infrastructure looks, and is, very complex and yet should be run and managed in a consistent and predictable way. - -If we create and change infrastructure components using the Web User Interface (UI) Console or even the gcloud command ine interface (CLI) tool, over time we won't be able to describe exactly in which `state` our infrastructure is in right now, meaning `we lose control over it`. - -This happens because you tend to forget what changes you've made a few months ago and why you made them. If multiple people across multiple teams are managing infrastructure, this makes things even worse. - -So we see here 2 clear problems: - -* we don't know the current state of our infrastructure -* we can't control the changes - -The second problem is dealt by source control tools like `git`, while the first one is solved by using tools like Terraform. Let's find out how. - -## Terraform - -Terraform is already installed on Google Cloud Shell. - -If you want to install it on a laptop or VM, you can [download here](https://www.terraform.io/downloads.html). - -Make sure Terraform version is => 0.11.0: - -```bash -$ terraform -v -``` - -## Infrastructure as Code project - -Create a new directory called `05-terraform` inside your `iac-tutorial` repo, which we'll use to save the work done in this lab. - -## Describe VM instance - -_Terraform allows you to describe the desired state of your infrastructure and makes sure your desired state meets the actual state._ - -Terraform uses [**resources**](https://www.terraform.io/docs/configuration/resources.html) to describe different infrastructure components. If you want to use Terraform to manage an infrastructure component, you should first make sure there is a resource for that component for that particular platform. - -Let's use Terraform syntax to describe a VM instance that we want to be running. - -Create a Terraform configuration file called `main.tf` inside the `05-terraform` directory with the following content: - -``` -resource "google_compute_instance" "node-svc" { - name = "node-svc" - machine_type = "f1-micro" - zone = "us-central1-c" - - # boot disk specifications - boot_disk { - initialize_params { - image = "node-svc-base" // use image built with Packer - } - } - - # networks to attach to the VM - network_interface { - network = "default" - access_config {} // use ephemeral public IP - } -} -``` - -Here we use [google_compute_instance](https://www.terraform.io/docs/providers/google/r/compute_instance.html) resource to manage a VM instance running in Google Cloud Platform. - -## Define Resource Provider - -One of the advantages of Terraform over other alternatives like [CloudFormation](https://aws.amazon.com/cloudformation/?nc1=h_ls) is that it's `cloud-agnostic`, meaning it can work with many different cloud providers like AWS, GCP, Azure, or OpenStack. It can also work with resources of different services like databases (e.g., PostgreSQL, MySQL), orchestrators (Kubernetes, Nomad) and [others](https://www.terraform.io/docs/providers/). - -This means that Terraform has a pluggable architecture and the pluggable component that allows it to work with a specific platform or service is called **provider**. - -So before we can actually create a VM using Terraform, we need to define a configuration of a [google cloud provider](https://www.terraform.io/docs/providers/google/index.html) and download it on our system. - -Create another file inside `terraform` folder and call it `providers.tf`. Put provider configuration in it: - -``` -provider "google" { - version = "~> 2.5.0" - project = "YOU MUST PUT YOUR PROJECT NAME HERE" - region = "us-central1-c" -} -``` - -Make sure to change the `project` value in provider's configuration above to your project's ID. You can get your default project's ID by running the command: - -```bash -$ gcloud config list project -``` - -Now run the `init` command inside `terraform` directory to download the provider: - -```bash -$ terraform init -``` - -## Bring Infrastructure to a Desired State - -Once we described a desired state of the infrastructure (in our case it's a running VM), let's use Terraform to bring the infrastructure to this state: - -```bash -$ terraform apply -``` - -After Terraform ran successfully, use a gcloud command to verify that the machine was indeed launched: - -```bash -$ gcloud compute instances describe node-svc -``` - -## Deploy Application - -We did provisioning via Terraform, but we still need to install and start our application. Let's do this remotely this time, instead of logging into the machine: - - -```bash -$ INSTANCE_IP=$(gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc) # get IP of VM -$ scp -r ../03-script/install.sh node-user@${INSTANCE_IP}:/home/node-user # copy install script -$ rsh ${INSTANCE_IP} -l node-user chmod +x /home/node-user/install.sh # set permissions -$ rsh ${INSTANCE_IP} -l node-user /home/node-user/install.sh # install app -$ rsh ${INSTANCE_IP} -l node-user sudo nodejs /home/node-user/node-svc-v1/server.js & # run app -``` - -NOTE: If you get an offending ECDSA key error, use the suggested removal command. - -NOTE: If you get the error `Permission denied (publickey).`, this probably means that your ssh-agent no longer has the node-user private key added. This easily happens if the Google Cloud Shell goes to sleep and wipes out your session. Check via issuing `ssh-add -l`. You should see something like `2048 SHA256:bII5VsQY3fCWXEai0lUeChEYPaagMXun3nB9U2eoUEM /home/betz4871/.ssh/node-user (RSA)`. If you do not, re-issue the command `ssh-add ~/.ssh/node-user` and re-confirm with `ssh-add -l`. - -Connect to the VM via SSH: - -```bash -$ ssh node-user@${INSTANCE_IP} -``` - -Check that servce is running, and then exit: - -```bash -node-user@node-svc:~$ curl localhost:3000 -Successful request. -node-user@node-svc:~$ exit -``` - -## Access the Application Externally7 - -Manually create the firewall rule: - -```bash -$ gcloud compute firewall-rules create allow-node-svc-tcp-3000 \ - --network default \ - --action allow \ - --direction ingress \ - --rules tcp:3000 \ - --source-ranges 0.0.0.0/0 -``` - - -Open another terminal and run the following command to get a public IP of the VM: - -```bash -$ gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc -``` - -Access the application in your browser by its public IP (don't forget to specify the port 3000). - - -## Add other GCP resources into Terraform - -Let's add ssh keys and the firewall rule into our Terraform configuration so that we know for sure those resources are present. - -First, delete the SSH project key and firewall rule: - -```bash -$ gcloud compute project-info remove-metadata --keys=ssh-keys -$ gcloud compute firewall-rules delete allow-node-svc-tcp-3000 -``` - -Make sure that your application became inaccessible via port 3000 and SSH connection with a private key of `node-user` fails. - -Then add appropriate resources into `main.tf` file. Your final version of `main.tf` file should look similar to this (change the ssh key file path, if necessary): - - -```bash -resource "google_compute_instance" "node-svc" { - name = "node-svc" - machine_type = "f1-micro" - zone = "us-central1-c" - - # boot disk specifications - boot_disk { - initialize_params { - image = "node-svc-base" // use image built with Packer - } - } - - # networks to attach to the VM - network_interface { - network = "default" - access_config {} // use ephemaral public IP - } -} - -resource "google_compute_project_metadata" "node-svc" { - metadata = { - ssh-keys = "node-user:${file("~/.ssh/node-user.pub")}" // path to ssh key file - } -} - -resource "google_compute_firewall" "node-svc" { - name = "allow-node-svc-tcp-3000" - network = "default" - allow { - protocol = "tcp" - ports = ["3000"] - } - source_ranges = ["0.0.0.0/0"] -} -``` - -Tell Terraform to apply the changes to bring the actual infrastructure state to the desired state we described: - -```bash -$ terraform apply -``` - -Using the same techniques as above, verify that the application became accessible again on port 3000 (locally and remotely) and SSH connection with a private key works. Here's a new way to check it from the Google Cloud Shell (you don't ssh into the VM): - -```bash -$ curl $INSTANCE_IP:3000 -``` - -## Create an output variable - -We have frequntly used this gcloud command to retrieve a public IP address of a VM: - -```bash -$ gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances describe node-svc -``` - -We can tell Terraform to provide us this information using [output variables](https://www.terraform.io/intro/getting-started/outputs.html). - -Create another configuration file inside `terraform` directory and call it `outputs.tf`. Put the following content in it: - -```json -output "node_svc_public_ip" { - value = "${google_compute_instance.node-svc.network_interface.0.access_config.0.nat_ip}" -} -``` - -Run terraform apply again, this time with auto approve: - -```bash -$ terraform apply -auto-approve - -google_compute_instance.node-svc: Refreshing state... [id=node-svc] -google_compute_firewall.node-svc: Refreshing state... [id=allow-node-svc-tcp-3000] -google_compute_project_metadata.node-svc: Refreshing state... [id=proven-sum-252123] -Apply complete! Resources: 0 added, 0 changed, 0 destroyed. -Outputs: -node_svc_public_ip = 34.71.90.74 -``` - -Couple of things to notice here. First, we did not destroy anything, so terraform refreshes - it confirms that configurations are still as specified. During this Terraform run, no resources have been created or changed, which means that the actual state of our infrastructure already meets the requirements of a desired state. - -Secondly, under "Outputs:", you should see the public IP of the VM we created. - -## Save and commit the work - -Save and commit the `05-terraform` folder created in this lab into your `iac-tutorial` repo. - -## Conclusion - -In this lab, you saw a state of the art the application of Infrastructure as Code practice. - -We used *code* (Terraform configuration syntax) to describe the *desired state* of the infrastructure. Then we told Terraform to bring the actual state of the infrastructure to the desired state we described. - -With this approach, Terraform configuration becomes *a single source of truth* about the current state of your infrastructure. Moreover, the infrastructure is described as code, so we can apply to it the same practices we commonly use in development such as keeping the code in source control, use peer reviews for making changes, etc. - -All of this helps us get control over even the most complex infrastructure. - -Destroy the resources created by Terraform and move on to the next lab. - -```bash -$ terraform destroy -auto-approve -``` - -Next: [Ansible](06-ansible.md) diff --git a/docs/06-ansible.md b/docs/06-ansible.md deleted file mode 100644 index d912cc1..0000000 --- a/docs/06-ansible.md +++ /dev/null @@ -1,267 +0,0 @@ -# Ansible - -In the previous lab, you used Terraform to implement Infrastructure as Code approach to managing the cloud infrastructure resources. There is another major type of tooling we need to consider, and that is **Configuration Management** (CM) tools. - -When talking about CM tools, we can often meet the acronym `CAPS` which stands for Chef, Ansible, Puppet and Saltstack - the most known and commonly used CM tools. In this lab, we're going to look at Ansible and see how CM tools can help us improve our operations. - -## Intro - -If you think about our current operations and what else there is to improve, you will probably see the potential problem in the deployment process. - -The way we do deployment right now is by connecting via SSH to a VM and running a deployment script. And the problem here is not the connecting via SSH part, but running a script. - -_Scripts are bad at long term management of system configuration, because they make common system configuration operations complex and error-prone._ - -When you write a script, you use a scripting language syntax (Bash, Python) to write commands which you think should change the system's configuration. And the problem is that there are too many ways people can write the code that is meant to do the same things, which is the reason why scripts are often difficult to read and understand. Besides, there are various choices as to what language to use for a script: should you write it in Ruby which your colleagues know very well or Bash which you know better? - -Common configuration management operations are well-known: copy a file to a remote machine, create a folder, start/stop/enable a process, install packages, etc. So _we need a tool that would implement these common operations in a well-known and tested way, providing us with a clean and understandable syntax for using them_. This way we wouldn't have to write complex scripts ourselves each time for the same tasks, possibly making mistakes along the way, but instead just tell the tool what should be done: what packages should be present, what processes should be started, etc. - -This is exactly what CM tools do. So let's check it out using Ansible as an example. - -## Install Ansible - -NOTE: this lab assumes Ansible v2.4 is installed. It may not work as expected with other versions as things change quickly. - -Issue the following commands in the Google cloud shell (note that Ansible will not remain installed when your shell goes to sleep): - -```bash -$ sudo apt update -$ sudo apt install software-properties-common -$ sudo apt-add-repository --yes --update ppa:ansible/ansible -$ sudo apt install -y ansible -``` - -If you have issues, reference the instructions on how to install Ansible on your system from [official documentation](http://docs.ansible.com/ansible/latest/intro_installation.html). - -Verify that Ansible was installed by checking the version: - -```bash -$ ansible --version -``` - -## Infrastructure as Code project - -Create a new directory called `06-ansible` inside your `iac-tutorial` repo, which we'll use to save the work done in this lab. - -## Provision compute resources - -Start a VM and create other GCP resources for running your application applying Terraform configuration you wrote in the previous lab (destroy first if you have some still running): - -```bash -$ cd ./05-terraform # adapt this command as necessary to get to the directory -$ terraform apply -auto-approve -``` - -## Deploy playbook - -We'll rewrite our Bash script used for deployment using Ansible syntax. - -Ansible uses **tasks** to define commands used for system configuration. Each Ansible task basically corresponds to one command in our Bash script. - -Each task uses some **module** to perform a certain operation on the configured system. Modules are well tested functions which are meant to perform common system configuration operations. - -Let's look at our `install.sh` first to see what modules we might need to use: - -```bash -#!/bin/bash -set -e # exit immediately if anything returns non-zero. See https://www.javatpoint.com/linux-set-command - - -echo " ----- download, initialize, and run app ----- " -git clone https://github.com/dm-academy/node-svc-v1 -cd node-svc-v1 -git checkout 02 -npm install -npm install express -``` - -We clearly see here several types of operations: cloning a git repo and setting the branch, initializing npm, and installing express (a Node package). - -We also, to start the service, need to run this command: - -`$ sudo nodejs /home/node-user/node-svc-v1/server.js &` - -So we'll search for Ansible modules that allow to perform these operations. Luckily, there are modules for all of these operations. - -Ansible uses YAML syntax to define tasks, which makes the configuration readable. - -Let's create a file called `deploy.yml` ("deploy" including both installation and launching) inside the `ansible` directory: - -```yaml ---- -- name: Deploy node-svc App - hosts: node-svc - tasks: - - name: Fetch the latest version of application code - # see https://docs.ansible.com/ansible/latest/modules/git_module.html - git: - repo: 'https://github.com/dm-academy/node-svc-v1' - dest: /home/node-user/node-svc-1 - version: "02" - register: clone - - - name: NPM install express and initialize app - # see https://docs.ansible.com/ansible/latest/modules/npm_module.html - npm: - name: express - global: yes - - - name: Install packages based on package.json. - npm: - path: /home/node-user/node-svc-1 - - - name: Start the nodejs server - # see https://codelike.pro/deploy-nodejs-app-with-ansible-git-pm2/ - sudo_user: node-user - command: pm2 start server.js --name node-app chdir=/home/node-user/node-svc-1s - ignore_errors: yes - when: npm_finished.changed - -``` - -In this configuration file, which is called a **playbook** in Ansible terminology, we define several tasks. - -The `name` that precedes each task is used as a comment that will show up in the terminal when the task starts to run. - -`register` option allows to capture the result output from running a task. - -The `first task` uses git module to pull the code from GitHub. - -```yaml -- name: Fetch the latest version of application code - # see https://docs.ansible.com/ansible/latest/modules/git_module.html - git: - repo: 'https://github.com/dm-academy/node-svc-v1' - dest: /home/node-user/node-svc-1 - version: 02 - register: git_finished -``` - - -The second task installs the npm package express and initializes the app in the specified directory: - -```yaml - - - name: NPM install express and initialize app - # see https://docs.ansible.com/ansible/latest/modules/npm_module.html - npm: - name: coffee-script - global: yes - - - name: Install packages based on package.json. - npm: - path: /home/node-user/node-svc-1 - -``` - -The third task runs the server: - -```yaml - - name: Start the nodejs server - # see https://codelike.pro/deploy-nodejs-app-with-ansible-git-pm2/ - sudo_user: node-user - command: pm2 start server.js --name node-app chdir=/home/node-user/node-svc-1s - ignore_errors: yes - when: npm_finished.changed - -``` - -Note, how for each module we use a different set of module options. You can find full information about the options in a module's documentation. - -In the second task, we use a conditional statement [when](http://docs.ansible.com/ansible/latest/playbooks_conditionals.html#the-when-statement) to make sure the `npm install` task is only run when the local repo was updated, i.e. the output from running git clone command was changed. This allows us to save some time spent on system configuration by not running unnecessary commands. - -On the same level as tasks, we also define a **handlers** block. Handlers are special tasks which are run only in response to notification events from other tasks. In our case, `node-svc` service gets restarted only when the `npm install` task is run. - -## Inventory file - -The way that Ansible works is simple: it connects to a remote VM (usually via SSH) and runs the commands that stand behind each module you used in your playbook. - -To be able to connect to a remote VM, Ansible needs information like IP address and credentials. This information is defined in a special file called [inventory](http://docs.ansible.com/ansible/latest/intro_inventory.html). - -Create a file called `hosts.yml` inside `ansible` directory with the following content (make sure to change the `ansible_host` parameter to public IP of your VM): - -```yaml -node-svc: - hosts: - node-svc-01: - ansible_host: 35.35.35.35 - ansible_user: node-user -``` - -Here we define a group of hosts (`node-svc`) under which we list the hosts that belong to this group. In this case, we list only one host under the hosts group and give it a name (`node-svc-01`) and information on how to connect to the host. - -Now note, that inside our `deploy.yml` playbook we specified `node-svc` host group in the `hosts` option before the tasks: - -```yaml ---- -- name: Deploy node-svc app - hosts: node-svc-01 - tasks: - ... -``` - -This will tell Ansible to run the following tasks on the hosts defined in hosts group `raddit-app`. - -## Ansible configuration - -Before we can run a deployment, we need to make some configuration changes to how Ansible views and manages our `ansible` directory. - -Let's define custom Ansible configuration for our directory. Create a file called `ansible.cfg` inside the `ansible` directory with the following content: - -```ini -[defaults] -inventory = ./hosts.yml -private_key_file = ~/.ssh/node-user -host_key_checking = False -``` - -This custom configuration will tell Ansible what inventory file to use, what private key file to use for SSH connection and to skip the host checking key procedure. - -## Run playbook - -Now it's time to run your playbook and see how it works. - -Use the following commands to start a deployment: - -```bash -$ cd ./06-ansible -$ ansible-playbook deploy.yml -``` - -## Access Application - -Access the application in your browser by its public IP (don't forget to specify the port 3000) and make sure application has been deployed and is functional. - -## Futher Learning Ansible - -There's a whole lot to learn about Ansible. Try playing around with it more and create a `playbook` which provides the same system configuration as your `configuration.sh` script. Save it under the name `configuration.yml` inside the `ansible` folder, then use it inside [ansible provisioner](https://www.packer.io/docs/provisioners/ansible.html) instead of shell in your Packer template. - -You can find an example of `configuration.yml` playbook [here](https://github.com/Artemmkin/infrastructure-as-code-example/blob/master/ansible/configuration.yml). - -And [here](https://github.com/Artemmkin/infrastructure-as-code-example/blob/master/packer/raddit-base-image-ansible.json) is an example of a Packer template which uses ansible provisioner. - -## Save and commit the work - -Save and commit the `ansible` folder created in this lab into your `iac-tutorial` repo. - -## Idempotence - -One more advantage of CM tools over scripts is that commands they implement designed to be **idempotent** by default. - -Idempotence in this case means that even if you apply the same configuration changes multiple times the result will stay the same. - -This is important because some commands that you use in scripts may not produce the same results when run more than once. So we always want to achieve idempotence for our configuration management system, sometimes applying conditionals statements as we did in this lab. - -## Conclusion - -Ansible provided us with a clean YAML syntax for performing common system configuration tasks. This allowed us to get rid of our own implementation of configuration commands. - -It might not seem like a big improvement at this scale, because our deploy script is small, but it definitely brings order to system configuration management and is more noticeable at medium and large scale. - -Destroy the resources created by Terraform. - -```bash -$ terraform destroy -``` - -Next: [Vagrant](07-vagrant.md) diff --git a/docs/07-vagrant.md b/docs/07-vagrant.md deleted file mode 100644 index ea3243b..0000000 --- a/docs/07-vagrant.md +++ /dev/null @@ -1,265 +0,0 @@ -## Vagrant (OPTIONAL, for local laptops/workstations only) - -In this lab, we're going to learn about [Vagrant](https://www.vagrantup.com/) which is another tool that implements IaC approach and is often used for creating development environments. - -_This lab is optional for those of you want to learn Vagrant on your personal laptops and workstations. It will not work on GCE virtual machines, university workstations, or Google Cloud Shell. The alternative to local development is to create a local development VM in the cloud. You may skip to [Docker](08-docker.md)_. - -## Intro - -Before this lab, our main focus was on how to create and manage an environment where our application runs and is accessible to the public. Let's call that environment `production` for the sake of simplicity of referring to that later. - -But what is about our local environment where we develop the code? Are there any problems with that? - -Running our application locally would require us installing all of its dependencies and configuring the local system pretty much the same way as we did in the previous labs. - -There are a few reasons why you don't want to do that: - -* `This can break your system`. When you change your system configuration there are lot of things that can go wrong. For example, when installing/removing different packages you can easily mess up the work of your system's package manager. -* `When something breaks in your system configuration, it can take a long time to fix`. If you've messed up with you local system configuration, you either need to debug or reinstall your OS. Both of these can take a lot of your time and should be avoided. -* `You have no idea what is your development environment actually looks like`. Your local OS will certainly have its own specific configuration and packages installed, because you use it for every day tasks different than just running your application. For this reason, even if your application works on your local machine, you cannot describe exactly what is required for it to run. This is commonly known as the `works on my machine` problem and is often one of the reasons for a conflict between Dev and Ops. - -Based on these problems, let's draw some requirements for our local dev environment: - -* `We should know exactly what is inside.` This is important, so that we could properly configure other environments for running the application. -* `Isolation from our local system.` This leaves us with choices of a local/remote VM or containers. -* `Ability to quickly and easily recreate when it breaks.` - -Vagrant is a tool that allows to meet all of these requirements. Let's find out how. - -## Install Vagrant and VirtualBox - -NOTE: this lab assumes Vagrant `v2.0.1` is installed. It may not work as expected on other versions. - -[Download](https://www.vagrantup.com/downloads.html) and install Vagrant on your system. - -Verify that Vagrant was successfully installed by checking the version: - -```bash -$ vagrant -v -``` - -[Download](https://www.virtualbox.org/wiki/Downloads) and install VirtualBox for running virtual machines locally. - -Also, make sure virtualization feature is enabled for your CPU. You would need to check BIOS settings for this. - -## Create a Vagrantfile - -If we compare Vagrant to the previous tools we've already learned, it reminds Terraform. Like Terraform, Vagrant allows you to declaratively describe VMs you want to provision, but it focuses on managing VMs (and containers) exclusively, so it's no good for things like firewall rules or VPC networks in the cloud. - -To start a local VM using Vagrant, we need to define its characteristics in a special file called `Vagrantfile`. - -Create a file named `Vagrantfile` inside `iac-tutorial` directory with the following content: - -```ruby -Vagrant.configure("2") do |config| - # define provider configuration - config.vm.provider :virtualbox do |v| - v.memory = 1024 - end - # define a VM machine configuration - config.vm.define "raddit-app" do |app| - app.vm.box = "ubuntu/xenial64" - app.vm.hostname = "raddit-app" - end -end -``` - -Vagrant, like Terraform, doesn't start VMs itself. It uses a `provider` component to communicate the instructions to the actual provider of infrastructure resources. - -In this case, we redefine Vagrant's default provider (VirtualBox) configuration to allocate 1024 MB of memory to each VM defined in this Vagrantfile: - -```ruby -# define provider configuration -config.vm.provider :virtualbox do |v| - v.memory = 1024 -end -``` - -We also specify characteristics of a VM we want to launch: what machine image (`box`) to use (Vagrant downloads a box from [Vagrant Cloud](https://www.vagrantup.com/docs/vagrant-cloud/boxes/catalog.html)), and what hostname to assign to a started VM: - -```ruby -# define a VM machine configuration -config.vm.define "raddit-app" do |app| - app.vm.box = "ubuntu/xenial64" - app.vm.hostname = "raddit-app" -end -``` - -## Start a Local VM - -With the Vagrantfile created, you can start a VM on your local machine using Ubuntu 16.04 image from Vagrant Cloud. - -Run the following command inside the folder with your Vagrantfile: - -```bash -$ vagrant up -``` - -Check the current status of the VM: - -```bash -$ vagrant status -``` - -You can connect to a started VM via SSH using the following command: - -```bash -$ vagrant ssh -``` - -## Configure Dev Environment - -Now that you have a VM running on your local machine, you need to configure it to run your application: install ruby, mongodb, etc. - -There are many ways you can do that, which are known to you by now. You can configure the environment manually, using scripts or some CM tool like Ansible. - -_It's best to use the same configuration and the same CM tools across all of your environments._ - -As we've already discussed, your application may work in your local environment, but it may not work on a remote VM running in production environment, because of the differences in configuration. But when your configuration is the same across all of your environments, the application will not fail for reasons like a missing package and the system configuration can generally be excluded as a potential cause of a failure when it occurs. - -Because we chose to use Ansible for configuring our production environment in the previous lab, let's use it for configuration management of our dev environment, too. - -Change your Vagrantfile to look like this: - -```ruby -Vagrant.configure("2") do |config| - # define provider configuration - config.vm.provider :virtualbox do |v| - v.memory = 1024 - end - # define a VM configuration - config.vm.define "raddit-app" do |app| - app.vm.box = "ubuntu/xenial64" - app.vm.hostname = "raddit-app" - # sync a local folder with application code to the VM folder - app.vm.synced_folder "raddit-app/", "/srv/raddit-app" - # use port forwarding make application accessible on localhost - app.vm.network "forwarded_port", guest: 9292, host: 9292 - # system configuration is done by Ansible - app.vm.provision "ansible" do |ansible| - ansible.playbook = "ansible/configuration.yml" - end - end -end -``` - -We added Ansible provisioning to the Vagrantfile which allows us to run a playbook for system configuration. - -```ruby -# system configuration is done by Ansible -app.vm.provision "ansible" do |ansible| - ansible.playbook = "ansible/configuration.yml" -end -``` - -In the previous lab, it was given to you as a task to create a `configuration.yml` playbook that provides the same functionality as `configuration.sh` script we had used before. If you did not do that, you can copy the playbook from [here](https://github.com/Artemmkin/infrastructure-as-code-example/blob/master/ansible/configuration.yml) (place it inside `ansible` directory). If you did create your own playbook, make sure you have a `pre_tasks` section as in [this example](https://github.com/Artemmkin/infrastructure-as-code-example/blob/master/ansible/configuration.yml). - -Note, that we also added a port forwarding rule for accessing our application and instructed Vagrant to sync a local folder with application code to a specified VM folder (`/srv/raddit-app`): - -```ruby -# sync a local folder with application code to the VM folder -app.vm.synced_folder "raddit-app/", "/srv/raddit-app" -# use port forwarding make application accessible on localhost -app.vm.network "forwarded_port", guest: 9292, host: 9292 -``` - -Now run the following command to configure the local dev environment: - -```bash -$ vagrant provision -``` - -Verify the configuration: - -```bash -$ vagrant ssh -$ ruby -v -$ bundle version -$ sudo systemctl status mongod -``` - -## Run Application Locally - -As we mentioned, we gave Vagrant the instruction to sync our folder with application to a VM's folder under the specified path. This way we can develop the application on our host machine using our favorite code editor and then run that code inside the VM. - -We need to first reload a VM for chages in our Vagrantfile to take effect: - -```bash -$ vagrant reload -``` - -Then connect to the VM to start application: - -```bash -$ vagrant ssh -$ cd /srv/raddit-app -$ sudo bundle install -$ puma -``` - -The application should be accessible to you now at the following URL: http://localhost:9292 - -Stop the application using `ctrl + C` keys. - -## Mess Up Dev Environment - -One of our requirements to local dev environment was that you can freely mess it up and recreate in no time. - -Let's try that. - -Delete Ruby on the VM: -```bash -$ vagrant ssh -$ sudo apt-get -y purge ruby -$ ruby -v -``` - -Try to run your application again (it should fail): - -```bash -$ cd /srv/raddit-app -$ puma -``` - -## Recreate Dev Environment - -Let's try to recreate our dev environment from scratch to see how big of a problem it will be. - -Run the following commands to destroy the current dev environment and create a new one: - -```bash -$ vagrant destroy -f -$ vagrant up -``` - -Once a new VM is up and running, try to launch your app in it: - -```bash -$ vagrant ssh -$ ruby -v -$ cd /srv/raddit-app -$ sudo bundle install -$ puma -``` - -The Ruby package should be present and the application should run without problems. - -Recreating a new dev environment was easy, took very little time and it didn't affect our host OS. That's exactly what we needed. - -## Save and commit the work - -Save and commit the Vagrantfile created in this lab into your `iac-tutorial` repo. - -## Conclusion - -Vagrant was able to meet our requirements for dev environments. It makes creating/recreating and configuring a dev environment easy and safe for our host operating system. - -Because we describe our local infrastructure in code in a Vagrantfile, we keep it in source control and make sure all our other colleagues have the same environment for the application as we do. - -Destroy the VM: - -```bash -$ vagrant destroy -f -``` - -Next: [Docker](08-docker.md) diff --git a/docs/08-docker.md b/docs/08-docker.md deleted file mode 100644 index bd10c2a..0000000 --- a/docs/08-docker.md +++ /dev/null @@ -1,157 +0,0 @@ -## Docker - -In this lab, we will talk about managing containers for the first time in this tutorial. Particularly, we will talk about [Docker](https://www.docker.com/what-docker) which is the most widely used platform for running containers. - -## Intro - -Remember when we talked about packer, we mentioned a few words about `Immutable Infrastructure` model? The idea was to package all application dependencies and application itself inside a machine image, so that we don't have to configure the system after start. Containers implement the same model, but they do it in a more efficient way. - -Containers allow you to create self-contained isolated environments for running your applications. - -They have some significant advantages over VMs in terms of implementing Immutable Infrastructure model: - -* `Containers are much faster to start than VMs.` Container starts in seconds, while a VM takes minutes. It's important when you're doing an update/rollback or scaling your service. -* `Containers enable better utilization of compute resources.` Very often computer resources of a VM running an application are underutilized. Launching multiple instances of the same application on one VM has a lot of difficulties: different application versions may need different versions of dependent libraries, init scripts require special configuration. With containers, running multiple instances of the same application on the same machine is easy and doesn't require any system configuration. -* `Containers are more lightweight than VMs.` Container images are much smaller than machine images, because they don't need a full operating system in order to run. In fact, a container image can include just a single binary and take just a few MBs of your disk space. This means that we need less space for storing the images and the process of distributing images goes faster. - -Let's try to implement `Immutable Infrastructure` model with Docker containers, while paying special attention to the `Dockerfile` part as a way to practice `Infrastructure as Code` approach. - -## (FOR PERSONAL LAPTOPS AND WORKSTATIONS ONLY) Install Docker Engine - -_Docker is already installed on Google Cloud Shell._ - -The [Docker Engine](https://docs.docker.com/engine/docker-overview/#docker-engine) is the daemon that gets installed on the system and allows you to manage containers with simple CLI. - -[Install](https://www.docker.com/community-edition) free Community Edition of Docker Engine on your system. - -Verify that the version of Docker Engine is => 17.09.0: - -```bash -$ docker -v -``` - -## (FOR ALL) Create Dockerfile - -You describe a container image that you want to create in a special file called **Dockerfile**. - -Dockerfile contains `instructions` on how the image should be built. Here are some of the most common instructions that you can meet in a Dockerfile: - -* `FROM` is used to specify a `base image` for this build. It's similar to the builder configuration which we defined in a Packer template, but in this case instead of describing characteristics of a VM, we simply specify a name of a container image used for build. This should be the first instruction in the Dockerfile. -* `ADD` and `COPY` are used to copy a file/directory to the container. See the [difference](https://stackoverflow.com/questions/24958140/what-is-the-difference-between-the-copy-and-add-commands-in-a-dockerfile) between the two. -* `RUN` is used to run a command inside the image. Mostly used for installing packages. -* `ENV` sets an environment variable available within the container. -* `WORKDIR` changes the working directory of the container to a specified path. It basically works like a `cd` command on Linux. -* `CMD` sets a default command, which will be executed when a container starts. This should be a command to start your application. - -Let's use these instructions to create a Docker container image for our node-svc application. - -Inside your `my-iac-tutorial` directory, create a directory called `08-docker`, and in it a text file called `Dockerfile` with the following content: - -``` -FROM node:11 -# Create app directory -WORKDIR /app -# Install app dependencies -# A wildcard is used to ensure both package.json AND package-lock.json are copied -# where available (npm@5+) -COPY package*.json ./ -RUN npm install -RUN npm install express -# If you are building your code for production -# RUN npm ci --only=production -# Bundle app source -COPY . /app -EXPOSE 3000 -CMD [ "node", "server.js" ] -``` - -This Dockerfile repeats the steps that we did multiple times by now to configure a running environment for our application and run it. - -We first choose an image that already contains Node of required version: -``` -# Use base image with node installed -FROM node:11 -``` - -The base image is downloaded from Docker official registry (storage of images) called [Docker Hub](https://hub.docker.com/). - -We then install required system packages and application dependencies: - -``` -# Install app dependencies -# A wildcard is used to ensure both package.json AND package-lock.json are copied -# where available (npm@5+)COPY package*.json ./ -RUN npm install -RUN npm install express -``` - -Then we copy the application itself. - -``` -# create application home directory and copy files -COPY . /app -``` - -Then we specify a default command that should be run when a container from this image starts: - -``` -CMD [ "node", "server.js" ] -``` - -## Build Container Image - -Once you defined how your image should be built, run the following command inside your `my-iac-tutorial` directory to create a container image for the node-svc application: - -```bash -$ docker build -t /node-svc-v1 . -``` - -The resulting image will be named `node-svc`. Find it in the list of your local images: - -```bash -$ docker images | grep node-svc -``` -At your option, you can save your build command in a script, such as `build.sh`. - -Now, run the container: - -```bash -$ docker run -d -p 8081:3000 /node-svc-v1 -``` - -Notice the "8081:3000" syntax. This means that while the container is running on port 3000 internally, it is externally exposed via port 8081. - -Again, you may wish to save this in a script, such as `run.sh`. - -Now, test the container: - -```bash -$ curl localhost:8081 -Successful request. -``` - -Again, you may wish to save this in a script, such as `test.sh`. - -## Save and commit the work - -Save and commit the files created in this lab. - -## Conclusion - -In this lab, you adopted containers for running your application. This is a different type of technology from what we used to deal with in the previous labs. Nevertheless, we use Infrastructure as Code approach here, too. - -We describe the configuration of our container image in a Dockerfile using Dockerfile's syntax. We then save that Dockefile in our application repository. This way we can build the application image consistently across any environments. - -Destroy the current playground before moving on to the next lab, through `docker ps`, `docker kill`, `docker images`, and `docker rmi`. In the example below, the container is named "beautiful_pascal". Yours will be different. Follow the example, substituting yours. - -```bash -$ docker ps -CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -64e60b7b0c81 charlestbetz/node-svc-v1 "docker-entrypoint.s…" 10 minutes ago Up 10 minutes 0.0.0.0:8081->3000/tcp beautiful_pascal -$ docker kill beautiful_pascal -$ docker images -# returns list of your images -$ docker rmi -f -``` - -Next: [Docker Compose](09-docker-compose.md) diff --git a/docs/09-docker-compose.md b/docs/09-docker-compose.md deleted file mode 100644 index 7ce228d..0000000 --- a/docs/09-docker-compose.md +++ /dev/null @@ -1,159 +0,0 @@ -## Docker Compose - -## PENDING CREATION OF node-svc MULTI-ARCHITECTURE -## DISREGARD RADDIT APP - -In the last lab, we learned how to create Docker container images using Dockerfile and implementing Infrastructure as Code approach. - -This time we'll learn how to describe in code and manage our local container infrastructure with [Docker Compose](https://docs.docker.com/compose/overview/). - -## Intro - -Remember how in the previous lab we had to use a lot of `docker` CLI commands in order to run our application locally? Specifically, we had to create a network for containers to communicate, a volume for container with MongoDB, launch MongoDB container, launch our application container. - -This is a lot of manual work and we only have 2 containers in our setup. Imagine how much work it would be to run a microservices application which includes a dozen of services. - -To make the management of our local container infrastructure easier and more reliable, we need a tool that would allow us to describe the desired state of a local environment and then it would create it from our description. - -**Docker Compose** is exactly the tool we need. Let's see how we can use it. - -## (FOR PERSONAL LAPTOPS AND WORKSTATIONS ONLY) Install Docker Compose - -Follow the official documentation on [how to install Docker Compose](https://docs.docker.com/compose/install/) on your system. - -Verify that installed version of Docker Compose is => 1.18.0: - -```bash -$ docker-compose -v -``` - -## Describe Local Container Infrastructure - -Docker Compose could be compared to Terraform, but it manages only Docker container infrastructure. It allows us to start containers, create networks and volumes, pass environment variables to containers, publish ports, etc. - -Let's use Docker Compose [declarative syntax](https://docs.docker.com/compose/compose-file/) to describe what our local container infrastructure should look like. - -Create a file called `docker-compose.yml` inside your `iac-tutorial` repo with the following content: - -```yml -version: '3.3' - -# define services (containers) that should be running -services: - mongo-database: - image: mongo:3.2 - # what volumes to attach to this container - volumes: - - mongo-data:/data/db - # what networks to attach this container - networks: - - raddit-network - - raddit-app: - # path to Dockerfile to build an image and start a container - build: . - environment: - - DATABASE_HOST=mongo-database - ports: - - 9292:9292 - networks: - - raddit-network - # start raddit-app only after mongod-database service was started - depends_on: - - mongo-database - -# define volumes to be created -volumes: - mongo-data: -# define networks to be created -networks: - raddit-network: -``` - -In this compose file, we define 3 sections for configuring different components of our container infrastructure. - -Under the **services** section we define what containers we want to run. We give each service a `name` and pass the options such as what `image` to use to launch container for this service, what `volumes` and `networks` should be attached to this container. - -If you look at `mongo-database` service definition, you should find it to be very similar to the docker command that we used to start MongoDB container in the previous lab: - -```bash -$ docker run --name mongo-database \ - --volume mongo-data:/data/db \ - --network raddit-network \ - --detach mongo:3.2 -``` - -So the syntax of Docker Compose can be easily understood by a person not even familiar with it [the documentation](https://docs.docker.com/compose/compose-file/#service-configuration-reference). - -`raddit-app` services configuration is a bit different from MongoDB service in a way that we specify a `build` option instead of `image` to build the container image from a Dockerfile before starting a container: - -```yml -raddit-app: - # path to Dockerfile to build an image and start a container - build: . - environment: - - DATABASE_HOST=mongo-database - ports: - - 9292:9292 - networks: - - raddit-network - # start raddit-app only after mongod-database service was started - depends_on: - - mongo-database -``` - -Also, note the `depends_on` option which allows us to tell Docker Compose that this `raddit-app` service depends on `mongo-database` service and should be started after `mongo-database` container was launched. - -The other two top-level sections in this file are **volumes** and **networks**. They are used to define volumes and networks that should be created: - -```yml -# define volumes to be created -volumes: - mongo-data: -# define networks to be created -networks: - raddit-network: -``` - -These basically correspond to the commands that we used in the previous lab to create a named volume and a network: - -```bash -$ docker volume create mongo-data -$ docker network create raddit-network -``` - -## Create Local Infrastructure - -Once you described the desired state of you infrastructure in `docker-compose.yml` file, tell Docker Compose to create it using the following command: - -```bash -$ docker-compose up -``` - -or use this command to run containers in the background: - -```bash -$ docker-compose up -d -``` - -## Access Application - -The application should be accessible to your as before via the web preview icon in Google Cloud Shell. `curl localhost:9292` will at least dump out the HTML (not very pretty, but if you see HTML you know the service is working to some degree at least). - -## Save and commit the work - -Save and commit the `docker-compose.yml` file created in this lab into your `iac-tutorial` repo. - -## Conclusion - -In this lab, we learned how to use Docker Compose tool to implement Infrastructure as Code approach to managing a local container infrastructure. This helped us automate and document the process of creating all the necessary components for running our containerized application. - -If we keep created `docker-compose.yml` file inside the application repository, any of our colleagues can create the same container environment on any system with just one command. This makes Docker Compose a perfect tool for creating local dev environments and simple application deployments. - -To destroy the local playground, run the following command: - -```bash -$ docker-compose down --volumes -``` - -Next: [Kubernetes](10-kubernetes.md) diff --git a/docs/10-kubernetes.md b/docs/10-kubernetes.md deleted file mode 100644 index b25b8f3..0000000 --- a/docs/10-kubernetes.md +++ /dev/null @@ -1,419 +0,0 @@ -## Kubernetes - -In the previous labs, we learned how to run Docker containers locally. Running containers at scale is quite different and a special class of tools, known as **orchestrators**, are used for that task. - -In this lab, we'll take a look at the most popular Open Source orchestration platform called [Kubernetes](https://kubernetes.io/) and see how it implements Infrastructure as Code model. - -## Intro - -We used Docker Compose to consistently create container infrastructure on one machine (our local machine). However, our production environment may include tens or hundreds of VMs to have enough capacity to provide service to a large number of users. What do you do in that case? - -Running Docker Compose on each VM from the cluster seems like a lot of work. Besides, if you want your containers running on different hosts to communicate with each other it requires creation of a special type of network called `overlay`, which you can't create using only Docker Compose. - -Moreover, questions arise as to: -* how to load balance containerized applications? -* how to perform container health checks and ensure the required number of containers is running? - -The world of containers is very different from the world of virtual machines and needs a special platform for management. - -Kubernetes is the most widely used orchestration platform for running and managing containers at scale. It solves the common problems (some of which we've mentioned above) related to running containers on multiple hosts. And we'll see in this lab that it uses the Infrastructure as Code approach to managing container infrastructure. - -Let's try to run our `raddit` application on a Kubernetes cluster. - -## (FOR PERSONAL LAPTOPS AND WORKSTATIONS ONLY) Install Kubectl - -Kubectl is installed on the Google Cloud Shell. - -[Kubectl](https://kubernetes.io/docs/reference/kubectl/overview/) is command line tool that we will use to run commands against the Kubernetes cluster. - -You can install `kubectl` onto your system as part of Google Cloud SDK by running the following command: - -```bash -$ gcloud components install kubectl -``` - -Check the version of kubectl to make sure it is installed: - -```bash -$ kubectl version -``` - -## Infrastructure as Code project - -Create a new directory called `kubernetes` inside your `iac-tutorial` repo, which we'll use to save the work done in this lab. - -## Describe Kubernetes cluster in Terraform - -We'll use [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/) (GKE) service to deploy a Kubernetes cluster of 3 nodes. - -We'll describe a Kubernetes cluster using Terraform so that we can manage it through code. - -Create a directory named `terraform` inside `kubernetes` directory. Create three files within it: - -```bash -variables.tf -terraform.tfvars -main.tf -``` - -### variables.tf -```bash -# Provider configuration variables -variable "project_id" { - description = "Project ID in GCP" -} - -variable "region" { - description = "Region in which to manage GCP resources" -} - -# Cluster configuration variables -variable "cluster_name" { - description = "The name of the cluster, unique within the project and zone" -} - -variable "zone" { - description = "The zone in which nodes specified in initial_node_count should be created in" -} - -``` -### terraform.tfvars -```bash -// define provider configuration variables -project_id = "some-project-ID" # project in which to create a cluster -region = "some-google-region" # region in which to create a cluster - -// define Kubernetes cluster variables -cluster_name = "iac-tutorial-cluster" # cluster name -zone = "some-google-zone" # zone in which to create a cluster nodes - -``` - -### main.tf -```bash -resource "google_container_cluster" "primary" { - name = "${var.cluster_name}" - location = "${var.zone}" - initial_node_count = 3 - - master_auth { - username = "" - password = "" - - client_certificate_config { - issue_client_certificate = false - } - } - - # configure kubectl to talk to the cluster - provisioner "local-exec" { - command = "gcloud container clusters get-credentials ${var.cluster_name} --zone ${var.zone} --project ${var.project_id}" - } - - node_config { - oauth_scopes = [ - "https://www.googleapis.com/auth/compute", - "https://www.googleapis.com/auth/devstorage.read_only", - "https://www.googleapis.com/auth/logging.write", - "https://www.googleapis.com/auth/monitoring", - ] - - metadata = { - disable-legacy-endpoints = "true" - } - - tags = ["iac-kubernetes"] - } - - timeouts { - create = "30m" - update = "40m" - } -} - -# create firewall rule to allow access to application -resource "google_compute_firewall" "nodeports" { - name = "node-port-range" - network = "default" - - allow { - protocol = "tcp" - ports = ["30000-32767"] - } - source_ranges = ["0.0.0.0/0"] -} - -``` - -We'll use this Terraform code to create a Kubernetes cluster. - -## Create Kubernetes Cluster - -`main.tf` holds all the information about the cluster that should be created. It's parameterized using Terraform [input variables](https://www.terraform.io/intro/getting-started/variables.html) which allow you to easily change configuration parameters. - -Look into `terraform.tfvars` file which contains definitions of the input variables and change them if necessary. You'll most probably want to change `project_id` value. - -``` -// define provider configuration variables -project_id = "infrastructure-as-code" # project in which to create a cluster -region = "europe-west1" # region in which to create a cluster - -// define Kubernetes cluster variables -cluster_name = "iac-tutorial-cluster" # cluster name -zone = "europe-west1-b" # zone in which to create a cluster nodes -``` - -After you've defined the variables, run Terraform inside `kubernetes/terraform` to create a Kubernetes cluster consisting of 2 nodes (VMs for running our application containers). - -```bash -$ gcloud services enable container.googleapis.com # enable Kubernetes Engine API -$ terraform init -$ terraform apply -``` - -Wait until Terraform finishes creation of the cluster. It can take about 3-5 minutes. - -Check that the cluster is running and `kubectl` is properly configured to communicate with it by fetching cluster information: - -```bash -$ kubectl cluster-info - -Kubernetes master is running at https://35.200.56.100 -GLBCDefaultBackend is running at https://35.200.56.100/api/v1/namespaces/kube-system/services/default-http-backend/proxy -... -``` - -## Deployment manifest - -Kubernetes implements Infrastructure as Code approach to managing container infrastructure. It uses special entities called **objects** to represent the `desired state` of your cluster. With objects you can describe - -* What containerized applications are running (and on which nodes) -* The compute resources available to those applications -* The policies around how those applications behave, such as restart policies, upgrades, and fault-tolerance - -By creating an object, you’re effectively telling the Kubernetes system what you want your cluster’s workload to look like; this is your cluster’s `desired state`. Kubernetes then makes sure that the cluster's actual state meets the desired state described in the object. - -Most of the times, you describe the object in a `.yaml` file called `manifest` and then give it to `kubectl` which in turn is responsible for relaying that information to Kubernetes via its API. - -**Deployment object** represents an application running on your cluster. We'll use it to run containers of our applications. - -Create a directory called `manifests` inside `kubernetes` directory. Create a `deployments.yaml` file inside it with the following content: - -```yaml -apiVersion: apps/v1beta1 # implies the use of kubernetes 1.7 - # use apps/v1beta2 for kubernetes 1.8 -kind: Deployment -metadata: - name: raddit-deployment -spec: - replicas: 2 - selector: - matchLabels: - app: raddit - template: - metadata: - labels: - app: raddit - spec: - containers: - - name: raddit - image: dmacademy/raddit - env: - - name: DATABASE_HOST - value: mongo-service ---- -apiVersion: apps/v1beta1 # implies the use of kubernetes 1.7 - # use apps/v1beta2 for kubernetes 1.8 -kind: Deployment -metadata: - name: mongo-deployment -spec: - replicas: 1 - selector: - matchLabels: - app: mongo - template: - metadata: - labels: - app: mongo - spec: - containers: - - name: mongo - image: mongo:3.2 -``` - -In this file we describe two `Deployment objects` which define what application containers and in what quantity should be run. The Deployment objects have the same structure so I'll briefly go over only one of them. - -Each Kubernetes object has 4 required fields: -* `apiVersion` - Which version of the Kubernetes API you’re using to create this object. You'll need to change that if you're using Kubernetes API version different than 1.7 as in this example. -* `kind` - What kind of object you want to create. In this case we create a Deployment object. -* `metadata` - Data that helps uniquely identify the object. In this example, we give the deployment object a name according to the name of an application it's used to run. -* `spec` - describes the `desired state` for the object. `Spec` configuration will differ from object to object, because different objects are used for different purposes. - -In the Deployment object's spec we specify, how many `replicas` (instances of the same application) we want to run and what those applications are (`selector`) - -```yml -spec: - replicas: 2 - selector: - matchLabels: - app: raddit -``` - -In our case, we specify that we want to be running 2 instances of applications that have a label `app=raddit`. **Labels** are used to give identifying attributes to Kubernetes objects and can be then used by **label selectors** for objects selection. - -We also specify a `Pod template` in the spec configuration. **Pods** are lower level objects than Deployments and are used to run only `a single instance of application`. In most cases, Pod is equal to a container, although you can run multiple containers in a single Pod. - -The `Pod template` which is a Pod object's definition nested inside the Deployment object. It has the required object fields such as `metadata` and `spec`, but it doesn't have `apiVersion` and `kind` fields as those would be redundant in this case. When we create a Deployment object, the Pod object(s) will be created as well. The number of Pods will be equal to the number of `replicas` specified. The Deployment object ensures that the right number of Pods (`replicas`) is always running. - -In the Pod object definition (`Pod template`) we specify container information such as a container image name, a container name, which is used by Kubernetes to run the application. We also add labels to identify what application this Pod object is used to run, this label value is then used by the `selector` field in the Deployment object to select the right Pod object. - -```yaml - template: - metadata: - labels: - app: raddit - spec: - containers: - - name: raddit - image: dmacademy/raddit - env: - - name: DATABASE_HOST - value: mongo-service -``` - -Notice how we also pass an environment variable to the container. `DATABASE_HOST` variable tells our application how to contact the database. We define `mongo-service` as its value to specify the name of the Kubernetes service to contact (more about the Services will be in the next section). - -Container images will be downloaded from Docker Hub in this case: the generic mongo container and the raddit image uploaded to the dmacademy organization. - -*It would be nice if we could use the locally built raddit image. Extra credit for anyone who can figure out how to do that.* - -## Create Deployment Objects - -Run a kubectl command to create Deployment objects inside your Kubernetes cluster (make sure to provide the correct path to the manifest file): - -```bash -$ kubectl apply -f manifests/deployments.yaml -``` - -Check the deployments and pods that have been created: - -```bash -$ kubectl get deploy -$ kubectl get pods -``` - -## Service manifests - -Running applications at scale means running _multiple containers spread across multiple VMs_. - -This arises questions such as: How do we load balance between all of these application containers? How do we provide a single entry point for the application so that we could connect to it via that entry point instead of connecting to a particular container? - -These questions are addressed by the **Service** object in Kubernetes. A Service is an abstraction which you can use to logically group containers (Pods) running in you cluster, that all provide the same functionality. - -When a Service object is created, it is assigned a unique IP address called `clusterIP` (a single entry point for our application). Other Pods can then be configured to talk to the Service, and the Service will load balance the requests to containers (Pods) that are members of that Service. - -We'll create a Service for each of our applications, i.e. `raddit` and `MondoDB`. Create a file called `services.yaml` inside `kubernetes/manifests` directory with the following content: - -```yaml -apiVersion: v1 -kind: Service -metadata: - name: raddit-service -spec: - type: NodePort - selector: - app: raddit - ports: - - protocol: TCP - port: 9292 - targetPort: 9292 - nodePort: 30100 ---- -apiVersion: v1 -kind: Service -metadata: - name: mongo-service -spec: - type: ClusterIP - selector: - app: mongo - ports: - - protocol: TCP - port: 27017 - targetPort: 27017 -``` - -In this manifest, we describe 2 Service objects of different types. You should be already familiar with the general object structure, so I'll just go over the `spec` field which defines the desired state of the object. - -The `raddit` Service has a NodePort type: - -```yaml -spec: - type: NodePort -``` - -This type of Service makes the Service accessible on each Node’s IP at a static port (NodePort). We use this type to be able to contact the `raddit` application later from outside the cluster. - -`selector` field is used to identify a set of Pods to which to route packets that the Service receives. In this case, Pods that have a label `app=raddit` will become part of this Service. - -```yaml - selector: - app: raddit -``` - -The `ports` section specifies the port mapping between a Service and Pods that are part of this Service and also contains definition of a node port number (`nodePort`) which we will use to reach the Service from outside the cluster. - -```yaml - ports: - - protocol: TCP - port: 9292 - targetPort: 9292 - nodePort: 30100 -``` - -The requests that come to any of your cluster nodes' public IP addresses on the specified `nodePort` will be routed to the `raddit` Service cluster-internal IP address. The Service, which is listening on port 9292 (`port`) and is accessible within the cluster on this port, will then route the packets to the `targetPort` on one of the Pods which is part of this Service. - -`mongo` Service is only different in its type. `ClusterIP` type of Service will make the Service accessible on the cluster-internal IP, so you won't be able to reach it from outside the cluster. - -## Create Service Objects - -Run a kubectl command to create Service objects inside your Kubernetes cluster (make sure to provide the correct path to the manifest file): - -```bash -$ kubectl apply -f manifests/services.yaml -``` - -Check that the services have been created: - -```bash -$ kubectl get svc -``` - -## Access Application - -Because we used `NodePort` type of service for the `raddit` service, our application should accessible to us on the IP address of any of our cluster nodes. - -Get a list of IP addresses of your cluster nodes: - -```bash -$ gcloud --format="value(networkInterfaces[0].accessConfigs[0].natIP)" compute instances list --filter="tags.items=iac-kubernetes" -``` - -Use any of your nodes public IP addresses and the node port `30100` which we specified in the service object definition to reach the `raddit` application in your browser. - -## Save and commit the work - -Save and commit the `kubernetes` folder created in this lab into your `iac-tutorial` repo. - -## Conclusion - -In this lab, we learned about Kuberenetes - a popular orchestration platform which simplifies the process of running containers at scale. We saw how it implements the Infrastructure as Code approach in the form of `objects` and `manifests` which allow you to describe in code the desired state of your container infrastructure which spans a cluster of VMs. - -To destroy the Kubernetes cluster, run the following command inside `kubernetes/terraform` directory: - -```bash -$ terraform destroy -``` - -Next: [What is Infrastructure as Code](50-what-is-iac.md) diff --git a/docs/50-what-is-iac.md b/docs/50-what-is-iac.md deleted file mode 100644 index cc8693d..0000000 --- a/docs/50-what-is-iac.md +++ /dev/null @@ -1,12 +0,0 @@ -# What is Infrastructure as Code? - -You've come a long way going through all the labs and learning about different Infrastructure as Code tools. Some sort of presentation of what Infrastructure as Code is should already be shaped in your head. - -To conclude this tutorial, I summarize some of the key points about what Infrastructure as Code means. - -1. `We use code to describe infrastructure`. We don't use UI to launch a VM, we decribe its desired characteristics in code and tell the tool to do that. -2. `Everyone is using the same tested code for infrastructure management operations and not creating its own implementation each time`. We talked about it when discussing downsides of scripts. Common infrastructure management operations should rely on tested code functions which are used in the team. It makes everyday operations more time efficient and less error-prone. -3. `Automated operations`. We don't run commands ourselves to launch and configure a system, but instead use a configuration syntax provided by IaC tool to tell it what should be done. -4. `We apply software development practices to infrastructure`. In software development, practices like keeping code in source control or peer reviews are very common. They make development reliable and working in a team possible. Since our infrastructure is described in code, we can apply the same practices to our infrastructure work. - -These are the points that I would make for now. If you feel like there is something else to add or change, please feel free to send a pull request :)