From 75976e234bf77f49bdb9c104f0220d1ac9315515 Mon Sep 17 00:00:00 2001
From: Marco Emilio Poleggi <marco-emilio.poleggi@hesge.ch>
Date: Tue, 1 Feb 2022 18:19:54 +0100
Subject: [PATCH] TF + AWS done! WIP: TF + SwitchEngines

---
 .gitattributes           |   1 +
 AWS/main.tf              |   1 +
 AWS/main.tf.advnc        | Bin 0 -> 1086 bytes
 AWS/main.tf.basic        |  24 ++
 AWS/outputs.tf           |  18 ++
 AWS/scripts/add-ssh.yaml |  16 ++
 AWS/variables.tf         |   5 +
 README-OpenStack.md      | 555 +++++++++++++++++++++++++++++++++++++++
 README.md                |  81 ++++--
 9 files changed, 680 insertions(+), 21 deletions(-)
 create mode 100644 .gitattributes
 create mode 120000 AWS/main.tf
 create mode 100644 AWS/main.tf.advnc
 create mode 100644 AWS/main.tf.basic
 create mode 100644 AWS/outputs.tf
 create mode 100644 AWS/scripts/add-ssh.yaml
 create mode 100644 AWS/variables.tf
 create mode 100644 README-OpenStack.md

diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..946c916
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+AWS/main.tf.advnc filter=git-crypt diff=git-crypt
diff --git a/AWS/main.tf b/AWS/main.tf
new file mode 120000
index 0000000..3dc4ae1
--- /dev/null
+++ b/AWS/main.tf
@@ -0,0 +1 @@
+main.tf.basic
\ No newline at end of file
diff --git a/AWS/main.tf.advnc b/AWS/main.tf.advnc
new file mode 100644
index 0000000000000000000000000000000000000000..8e2ffa805c8b2dde454c23c0e2d31b6caee096fa
GIT binary patch
literal 1086
zcmZQ@_Y83kiVO&0IOHLvmU&c8`_#vv1+wR|+;7SRB-q{MVP3u0j&ViM#+%!l_^bpv
zS4pxT-?>Yn{7&&>ll+yQdt$EN&0x@<l@>SgONPyB|C>vmA6T+c>Y|aqYI#6>^OE_t
zJ^eeXvhB8hT{>raZTa){7w4UpH94r^GxKw-fMEHL3k&|a<y9U(|5{b2XToHg+II)t
zzo=#`iZ$tbxIf*v<Ahbn!$209b5FAV-C4NwP1Kr9nQbqBl*np@o9$Yq$@k^bZ!Wi{
zFFSi9#C%^#mTZ6fyslTg(1|0E*HydWSY^rOcGqu*ICNyDX0w_en|*#t|CVWqM(0=P
zTWc0qym&M9UUhs~@`sn7?GK51?$x`xOZSh_A))xl?el`BW<~m~*-&zM-x=Q(=FU^M
z+VU@N%l7|eD&n^<AZYc&6H4pZ(%qF=&L0oa_T9;AwtL27-Otu{ohRA8;Js{7%NCQZ
zu~GPp+*;E;O&^?Adr!Q@ksbAcqbDlj%Db(Nk_Upix%LV@V6Xl%Um|Pf_T7^^?tZmi
zmd91pIyZH_kL!WmaUymPC){H*PP&%ys*=6(lg1AtDV4*`dncb)UAyv*=d6$6(=6mB
zU*&pMXex4t!LE6B&#WmuzRv4zZh5S`+{g7?yMlqe<F6Av!DqHFxD+{+;Rk<6=u-O-
z|8lF0nh9Mi<!q<C6-#uDkz^6?W;-{jxv-eMJ6Ljw)%!_~E3Z$x;`Y((?uCSP)>ZBm
z_wUZ%dD>rgYP;T_NS)__O&`Pi?|i@0UKC$H^M{&Y=eL=A-7Hz|Xs-FL7M7y4;Qp-q
z2*+7>%sH$W4F#XBV6E<xes|91$K6MVLJVa$uzg-~!NDwL&(<9+3%$Q9+Me;ad4}oY
zr#bBW@xLBqE<f<%vQ+x7$>|yO>5Mz(y!|dd>$5=e4Yt?1Ms~Bgcm18S@oPs4v-|B`
ztR3e3>KoNHy5AJl`>l8EnW3y?dRjQ*w@$}J)oB;yKD^(qoVem0XM5XyugDD@*4);;
za<}I*7fxAkxp(i%)vx;EuZPKQow?sJ=5x?fFKO1g-)sC`wx2YV|4_Q?=L(gk*txdR
zY-=jrlulPJJv%{GLCRD9T28pm!8_AaE*)4l(QfsA<5MfUJvFAj=JVP)XN&W)sD@LY
zMcITn*}0~!sr$^7z3W`H&{v5cjX&;pmu{QOSo20Z@vM^O+UFOJwm%e#h+$0Qe7NC?
zg>=M>pr9k0>n4a@Y+Nwmceu;@Gb=XfGCx#c%iP(&owxGoN5vkajOst(AG8l&`)q&X
zt*MvTT$dBi%DC)jSC%pCnkqeck@uTTjy8EB`5U`Z7gZ~=u}iT|X?t_(rAW*Ed#j|o
zXL=VX8ZNDsk~x^Zy~oGl@24~s3wfWb|G$^-*nTCrBDYk1Eu-``^{8X9exJUbpL*%g
zx!?15^9%f`DRlZ~xOADO`|Y{!wj94uu3X7{D`NTbsac!X-v3{h_G7`H?Z0CTH=0Ff
J1Y9(!1puxS81Dc8

literal 0
HcmV?d00001

diff --git a/AWS/main.tf.basic b/AWS/main.tf.basic
new file mode 100644
index 0000000..15c75e7
--- /dev/null
+++ b/AWS/main.tf.basic
@@ -0,0 +1,24 @@
+terraform {
+  required_providers {
+    aws = {
+      source  = "hashicorp/aws"
+      version = "~> 3.27"
+    }
+  }
+
+  required_version = ">= 0.14.9"
+}
+
+provider "aws" {
+  profile = "default"
+  region  = "us-east-1"
+}
+
+resource "aws_instance" "app_server" {
+  ami           = "<your-EC2-AMI-ID>"
+  instance_type = "t2.micro"
+
+  tags = {
+    Name = "ExampleAppServerInstance"
+  }
+}
diff --git a/AWS/outputs.tf b/AWS/outputs.tf
new file mode 100644
index 0000000..68dd48b
--- /dev/null
+++ b/AWS/outputs.tf
@@ -0,0 +1,18 @@
+output "instance_id" {
+  description = "ID of the EC2 instance"
+  value       = aws_instance.app_server.id
+}
+
+output "instance_public_ip" {
+  description = "Public IP address of the EC2 instance"
+  value       = aws_instance.app_server.public_ip
+}
+
+output "public_ip" {
+  value       = aws_instance.app_server.public_ip
+}
+
+output "instance_tags" {
+  description = "Public IP address of the EC2 instance"
+  value       = aws_instance.app_server.tags_all
+}
diff --git a/AWS/scripts/add-ssh.yaml b/AWS/scripts/add-ssh.yaml
new file mode 100644
index 0000000..2e33918
--- /dev/null
+++ b/AWS/scripts/add-ssh.yaml
@@ -0,0 +1,16 @@
+#cloud-config
+#^^^^^^^^^^^^
+# DO NOT TOUCH the first line!
+---
+groups:
+  - ubuntu: [root, sys]
+  - terraform
+
+users:
+  - default
+  - name: terraform
+    gecos: terraform
+    primary_group: terraform
+    groups: users, admin
+    ssh_authorized_keys:
+      - <your-SSH-pub-key-on-one-line>
diff --git a/AWS/variables.tf b/AWS/variables.tf
new file mode 100644
index 0000000..e3efb21
--- /dev/null
+++ b/AWS/variables.tf
@@ -0,0 +1,5 @@
+variable "instance_name" {
+  description = "Value of the Name tag for the EC2 instance"
+  type        = string
+  default     = "AnotherAppServerInstance"
+}
\ No newline at end of file
diff --git a/README-OpenStack.md b/README-OpenStack.md
new file mode 100644
index 0000000..3e47c0e
--- /dev/null
+++ b/README-OpenStack.md
@@ -0,0 +1,555 @@
+## Lab: Cloud provisioning/orchestration - Terraform and AWS
+
+Lab with Terraform and any Cloud
+
+Lab template for a Cloud provisioning/orchestration exercise with Terraform
+(TF) and OpenStack/SwitchEngines.
+
+## Pedagogical objectives ##
+
+  * Become familiar with a Cloud provisioning/orchestration tool
+  * Provision Cloud resources in an automated fashion
+
+## Tasks ##
+
+In this lab you will perform a number of tasks and document your progress in a
+lab report. Each task specifies one or more deliverables to be
+produced. Collect all the deliverables in your lab report.
+
+**N.B.** Some tasks require interacting with your local machine's OS: any
+related commands are supposed to be run into a terminal with the following
+conventions about the *command line prompt*:
+
+  * `#`: execution with super user's (root) privileges
+  * `$`: execution with normal user's privileges
+  * `lcl`: your local machine
+  * `ins`: your VM instance
+
+TF CLI commands' output follow a diff-style convention of prefixing lines with
+special marks to explain what is (or would be) going on:
+  * `+`: something is added
+  * `-`: something is removed/destroyed
+  * `-/+`: a resource is destroyed and recreated
+  * `~`: something is modified in place
+
+
+### Task #1: install Terraform CLI and OpenStack CLI ###
+
+**Goal:** install the Terraform CLI and OpenStack CLI on your local machine.
+Please, refer to your OS documentation for the proper way to do so
+
+  1. [Terraform
+     CLI](https://learn.hashicorp.com/tutorials/terraform/install-cli)
+     v1.1.4. Skip the TF "Quick start tutorial" (Docker).
+  1. [OpenStack CLI](https://docs.openstack.org/newton/user-guide/common/cli-install-openstack-command-line-clients.html) v5.7.0.
+  1. Create a new [OpenStack Access
+     Credentials](https://engines.switch.ch/horizon/identity/application_credentials/)
+     and save as `~/.config/openstack/clouds.yaml`. With SwitchEngine, the
+     cloud name to use in this lab should be `engines`.
+  1. Verify that your credentials are OK:
+  ``` shell
+  lcl$ $ openstack --os-cloud=engines [application] credential list
+  ```
+
+### Task #2: configure TF for OpenStack ###
+
+**Goal:** instruct TF to handle a single OpenStack instance.
+
+<a name="image-query"></a>Find out the smallest image to use for a Debian server:
+
+``` shell
+lcl$ openstack --os-cloud=engines image list --limit=20 --public --status=active  --sort-column=Size -c ID -c Name -c Size --long
++--------------------------------------+-------------------------------------+-------------+
+| ID                                   | Name                                |        Size |
++--------------------------------------+-------------------------------------+-------------+
+| c6596c8a-b074-4a72-9c8c-411d1cb11113 | Debian Buster 10 (SWITCHengines)    |  1537409024 |
+...
+```
+
+:bulb: We use the first ID found for the placeholder `<your-image-ID>`. In
+SwitchEngines this is 1.5GB.
+
+Find out the smallest instance *flavor* that acommodates our Debian image.
+
+  ``` shell
+  lcl$ openstack --os-cloud=engines flavor list --sort-column=RAM --sort-column=Disk --min-disk=5 --min-ram=1024 --limit=1 --public
++----+----------+------+------+-----------+-------+-----------+
+| ID | Name     |  RAM | Disk | Ephemeral | VCPUs | Is Public |
++----+----------+------+------+-----------+-------+-----------+
+| 2  | m1.small | 2048 |   20 |         0 |     1 | True      |
++----+----------+------+------+-----------+-------+-----------+
+  ```
+
+:bulb: Our flavor will be `m1.small` for the placeholder `<your-flavor>`.
+
+
+**@@@ RESTART FROM HERE @@@**
+
+Create a "sandbox" directory on your local machine `~/terraform/AWS/`. Inside
+it, create a file called `main.tf` (written in HCL language), the
+infrastructure *definition* file, with the following content:
+
+``` hcl
+terraform {
+  required_providers {
+    aws = {
+      source  = "hashicorp/aws"
+      version = "~> 3.27"
+    }
+  }
+
+  required_version = ">= 0.14.9"
+}
+
+provider "aws" {
+  profile = "default"
+  region  = "us-east-1"
+}
+
+resource "aws_instance" "app_server" {
+  ami           = "<your-image-ID>"
+  instance_type = "t2.micro"
+
+  tags = {
+    Name = "ExampleAppServerInstance"
+  }
+}
+```
+
+Initialize your sandbox with:
+
+``` shell
+lcl$ terraform init
+
+Initializing the backend...
+
+Initializing provider plugins...
+- Finding hashicorp/aws versions matching "~> 3.27"...
+- Installing hashicorp/aws v3.27.0...
+- Installed hashicorp/aws v3.27.0 (signed by HashiCorp)
+
+Terraform has created a lock file .terraform.lock.hcl to record the provider
+selections it made above. Include this file in your version control repository so
+that Terraform can guarantee to make the same selections by default when you run
+"terraform init" in the future.
+
+Terraform has been successfully initialized!
+...
+```
+
+Have a look inside the newly created sub-directory
+`~/terraform/AWS/.terraform/`, you'll find the required `aws` provider module
+that has been downloaded during the initialization.
+
+It's good practice to format and validate your configuration:
+
+``` shell
+lcl$ terraform fmt
+main.tf
+lcl$ terraform validate
+Success! The configuration is valid.
+```
+
+### Task #3: deploy your AWS infrastructure ###
+
+**Goal:** provision your AWS EC2 instance via TF.
+
+Run the following command, confirm by typing "yes" and observe it's output:
+
+``` shell
+lcl$ terraform apply
+
+Terraform used the selected providers to generate the following execution plan.
+Resource actions are indicated with the following symbols:
+  + create
+
+Terraform will perform the following actions:
+
+  # aws_instance.app_server will be created
+  + resource "aws_instance" "app_server" {
+      + ami                                  = "ami-0fa37863afb290840"
+      + arn                                  = (known after apply)
+      ...
+      + instance_type                        = "t2.micro"
+      ...
+      + tags                                 = {
+          + "Name" = "ExampleAppServerInstance"
+        }
+      ...
+
+Plan: 1 to add, 0 to change, 0 to destroy.
+
+Do you want to perform these actions?
+  Terraform will perform the actions described above.
+  Only 'yes' will be accepted to approve.
+
+  Enter a value: yes
+
+aws_instance.app_server: Creating...
+...
+aws_instance.app_server: Creation complete after 38s [id=i-0155ba9d77ee0a854]
+
+Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
+```
+
+The information shown above before the prompt for action is the `execution
+plan`: the `+` prefix mark things to add. Of course, many details are unknown
+until the corresponding resource is instantiated.
+
+:question: How many resources were created?
+
+You can verify via the AWS dashboard that one EC2 instance has been created.
+
+The state of the resources acted upon is locally stored. Let's see what's in
+our sandbox:
+``` shell
+lcl$ tree -a
+.
+├── .terraform
+│   └── providers
+...
+├── .terraform.lock.hcl
+├── main.tf
+└── terraform.tfstate
+```
+
+:question: What's in file `terraform.tfstate`? The answer comes from the
+following commands:
+
+``` shell
+lcl$ terraform state list
+aws_instance.app_server
+```
+It confirms that we're tracking one AWS instance. Let's dig a bit more:
+
+``` shell
+lcl$ terraform show
+# aws_instance.app_server:
+resource "aws_instance" "app_server" {
+    ami                                  = "ami-0fa37863afb290840"
+    arn                                  = "arn:aws:ec2:us-east-1:768034348959:instance/i-0155ba9d77ee0a854"
+    associate_public_ip_address          = true
+    availability_zone                    = "us-east-1e"
+    ...
+    id                                   = "i-0155ba9d77ee0a854"
+    instance_initiated_shutdown_behavior = "stop"
+    instance_state                       = "running"
+    instance_type                        = "t2.micro"
+    ...
+    private_dns                          = "ip-172-31-94-207.ec2.internal"
+    private_ip                           = "172.31.94.207"
+    public_dns                           = "ec2-3-94-184-169.compute-1.amazonaws.com"
+    public_ip                            = "3.94.184.169"
+    ...
+    tags                                 = {
+        "Name" = "ExampleAppServerInstance"
+    }
+    ...
+    vpc_security_group_ids               = [
+        "sg-0c420780b4f729d3e",
+    ]
+    ...
+}
+```
+
+The above command output provides useful runtime (state) information, like the
+instance IP's address. Indeed, there is a kind of *digital twin* stored inside
+the file `terraform.tfstate`.
+
+:question: Is that instance accessible via SSH? Give it a try. If not, why?
+
+Now, stop the running instance (its ID is shown above ;-):
+
+``` shell
+lcl$ aws ec2 stop-instances --instance-ids i-0155ba9d77ee0a854
+```
+
+wait some seconds and test again:
+
+``` shell
+lcl$ terraform show | grep instance_state
+   instance_state                       = "running"
+```
+
+How come? We just discovered that TF read only the local status of a
+resource. So, let's refresh it, and check again:
+
+``` shell
+lcl$ terraform refresh
+aws_instance.app_server: Refreshing state... [id=i-0155ba9d77ee0a854]
+lcl$ terraform show | grep instance_state
+    instance_state                       = "stopped"
+```
+
+Ah-ha!
+
+Hold on a second: our TF plan does not specify the desired status of a
+resource. What happens if we reapply the plan? Lets' try:
+
+``` shell
+lcl$ terraform apply -auto-approve
+aws_instance.app_server: Refreshing state... [id=i-0155ba9d77ee0a854]
+
+No changes. Your infrastructure matches the configuration.
+
+Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.
+
+Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
+lcl$ terraform show | grep instance_state
+    instance_state                       = "stopped"
+```
+
+:warning: Apply was run in `-auto-approve` (non interactive) mode which
+assumes "yes" to all questions. Use with care!
+
+From the above commands' output we see that
+  * the local state is refreshed before doing anything,
+  * no changes are applied, and
+  * huh?... the resource is still stopped.
+
+Concerning the last point above, think about the basic objectives of TF: as a
+provisioning tool it is concerned with the *existence* of a resource, not with
+its *runtime* state. This latter is the business of configuration management
+tools. :bulb: There is no way with TF to specify a resource's desired runtime
+state.
+
+
+### Task #4: change your infrastructure ###
+
+**Goal:** modify the resource created before, and learn how to apply changes
+to a Terraform project.
+
+Restart your managed instance:
+
+``` shell
+lcl$ aws ec2 start-instances --instance-ids i-0155ba9d77ee0a854
+```
+
+Refresh TF's view of the world:
+
+``` shell
+lcl$ terraform refresh
+aws_instance.app_server: Refreshing state... [id=i-0155ba9d77ee0a854]
+lcl$ terraform show | grep instance_state
+    instance_state                       = "running"
+```
+
+Replace the resource's `ami` in `main.tf` with the second one found from the
+[catalog query done above](#image-query) (or another one available with your
+account). Before applying our new plan, let's see what TF thinks of it:
+
+``` shell
+lcl$ terraform plan -out=change-AMI.tfplan
+aws_instance.app_server: Refreshing state... [id=i-0155ba9d77ee0a854]
+
+Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+-/+ destroy and then create replacement
+
+Terraform will perform the following actions:
+
+  # aws_instance.app_server must be replaced
+-/+ resource "aws_instance" "app_server" {
+      ~ ami                                  = "ami-0fa37863afb290840" -> "ami-0e2512bd9da751ea8" # forces replacement
+
+...
+
+Plan: 1 to add, 0 to change, 1 to destroy.
+
+Saved the plan to: change-AMI.tfplan
+
+To perform exactly these actions, run the following command to apply:
+    terraform apply "change-AMI.tfplan"
+```
+
+:bulb: Remarks:
+  * The change we want to apply is destructive!
+  * We saved our plan. :question: Why? It is not really necessary in a simple
+    scenario like ours, however a more complex IaC workflow might require plan
+    artifacts to be programmatically validated and versioned.
+
+Apply the saved plan:
+``` shell
+lcl$ terraform apply change-AMI.tfplan
+aws_instance.app_server: Destroying... [id=i-0155ba9d77ee0a854]
+...
+aws_instance.app_server: Destruction complete after 33s
+aws_instance.app_server: Creating...
+...
+aws_instance.app_server: Creation complete after 48s [id=i-0470db35749548101]
+```
+
+:bulb: What? Not asking for confirmation? Indeed, a saved plan is intended for
+automated workflows! Moreover, a saved plan will come handy for rolling back a
+broken infrastructure to the last working setup.
+
+:question: What if we did not save our plan, and called a plain apply command?
+Would the result be the same?
+
+
+### Task #5: input variables ###
+
+**Goal:** make a TF plan more flexible via input variables.
+
+Our original plan has all its content hard-coded. Let's make it more flexible
+with some input variables stored in a separate `variables.tf` file inside your
+TF sandbox:
+
+``` hcl
+variable "instance_name" {
+  description = "Value of the Name tag for the EC2 instance"
+  type        = string
+  default     = "AnotherAppServerInstance"
+}
+```
+
+Then modify the `main.tf` as follows:
+
+``` hcl
+resource "aws_instance" "app_server" {
+  ami           = "ami-0e2512bd9da751ea8"
+  instance_type = "t2.micro"
+
+  tags = {
+-   Name = "ExampleAppServerInstance"
++   Name = var.instance_name
+  }
+}
+```
+
+Apply the changes:
+``` shell
+lcl$ terraform apply -auto-approve
+aws_instance.app_server: Refreshing state... [id=i-0470db35749548101]
+...
+
+  ~ update in-place
+
+Terraform will perform the following actions:
+
+  # aws_instance.app_server will be updated in-place
+  ~ resource "aws_instance" "app_server" {
+        id                                   = "i-0470db35749548101"
+      ~ tags                                 = {
+          ~ "Name" = "ExampleAppServerInstance" -> "AnotherAppServerInstance"
+        }
+      ~ tags_all                             = {
+          ~ "Name" = "ExampleAppServerInstance" -> "AnotherAppServerInstance"
+        }
+...
+    }
+
+Plan: 0 to add, 1 to change, 0 to destroy.
+...
+Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
+```
+
+:bulb: **Exercise:** input variables can also be passed on the `apply` command
+line. Find how to do that with another different value for the variable
+`instance_name`. :question: Would this last change be persistent if we rerun a
+plain `terraform apply`?
+
+
+### Task #6: queries with outputs ###
+
+**Goal:** use output values to query a provisioned infrastructure.
+
+We have seen in the previous tasks that the infrastructure's status can be
+displayed via `terraform show`: a rather clumsy way, if you just want to
+extract some specific information. A better programmatic way of querying your
+infrastructure makes use of "outputs". Put the following in a file called
+`~/terraform/AWS/outputs.tf`:
+
+``` hcl
+output "instance_id" {
+  description = "ID of the EC2 instance"
+  value       = aws_instance.app_server.id
+}
+
+output "instance_public_ip" {
+  description = "Public IP address of the EC2 instance"
+  value       = aws_instance.app_server.public_ip
+}
+```
+
+We have declared two outputs. As usual with TF, before querying their
+associated values, we need to apply the changes:
+
+``` shell
+lcl$ terraform apply -auto-approve
+...
+
+Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
+
+Outputs:
+
+instance_id = "i-0470db35749548101"
+instance_public_ip = "34.201.252.63"
+instance_tags = tomap({
+  "Name" = "AnotherAppServerInstance"
+})
+```
+
+So, we already got the needed information, but, within a workflow, it is more
+practical to do something like:
+
+``` shell
+lcl$ terraform output -json instance_tags
+{"Name":"AnotherAppServerInstance"}
+```
+
+:question: What if the `Name` tag is changed outside TF? Try it.
+
+:question: What must be done to have TF respect that external change?
+
+:question: How to revert an external change via TF?
+
+
+### Task #7: SSH provisioning with Cloud-Init ###
+
+**Goal:** use Cloud-Init to provision an SSH access to your TF-managed
+instance.
+
+Did you try to SSH into the instance you created via TF? It cannot work,
+because we did not instructed TF about networks, users, keys or anything
+else. **This is left entirely to you as an exercise.** You need to:
+
+  1. Destroy your infrastructure. There's a special TF command for that.
+  1. Create an SSH key pair `tf-cloud-init`.
+  1. Create a new cloud-init file
+     `~/terraform/AWS/scripts/add-ssh.yaml` with the following content:
+     ``` yaml
+     #cloud-config
+     #^^^^^^^^^^^^
+     # DO NOT TOUCH the first line!
+     ---
+     groups:
+       - ubuntu: [root, sys]
+       - terraform
+
+     users:
+       - default
+       - name: terraform
+         gecos: terraform
+         primary_group: terraform
+         groups: users, admin
+         ssh_authorized_keys:
+           - <your-SSH-pub-key-on-one-line>
+     ```
+     :warning: **Mind that the first line of this file must spell exactly
+     `#cloud-config`**!
+  1. Modify the `main.tf` as follows:
+     1. add a `resource` block of type `"aws_security_group"` allowing ingress
+        ports 22 and 80 from any address, with any egress port open;
+     1. add a `data` block referencing the above authorization file as a
+        `"template_file"` type;
+     1. extend the `"aws_instance"` resource to:
+        1. associate a public IP address,
+        1. link the `data` block to a user data attribute;
+     1. add an output `"public_ip"`.
+
+When done, *init* your new plan, *validate* and *apply* it. Verify that you
+can SSH as user `terraform` (not the customary `ubuntu`) into your instance:
+
+``` shell
+lcl$ ssh terraform@$(terraform output -raw public_ip) -i ../tf-cloud-init
+```
diff --git a/README.md b/README.md
index c56f8c6..f0ebdf5 100644
--- a/README.md
+++ b/README.md
@@ -58,6 +58,23 @@ AWS](https://learn.hashicorp.com/tutorials/terraform/aws-build).
 
 **Goal:** instruct TF to handle a single AWS EC2 instance.
 
+<a name="AMI-query"></a>Find out which AMI to use for a recent Ubuntu server.
+You can query the AMI Catalog via
+  a. [AWS dashboard](https://console.aws.amazon.com/ec2/v2/home?region=us-east-1#AMICatalog:), searching "Ubuntu", selecting "Quickstart AMIs" and filtering by "Free tier",
+  b. or with CLI, and get the 2 most recent AMIs (it will not list free-tier ones):
+  ``` shell
+  lcl$ aws ec2 describe-images --region us-east-1 \
+    --filters "Name=root-device-type,Values=ebs" \
+              "Name=name,Values=*ubuntu*20*server*" \
+              "Name=architecture,Values=x86_64" \
+    --query "reverse(sort_by(Images, &ImageId))[:2].[ImageId]" --output text
+  ami-0ff99e17387586219
+  ami-0fe93fb38d72b89b6
+  ```
+
+:bulb: In the following task(s), we use the first AMI found for the
+placeholder `<your-EC2-AMI-ID>`.
+
 Create a "sandbox" directory on your local machine `~/terraform/AWS/`. Inside
 it, create a file called `main.tf` (written in HCL language), the
 infrastructure *definition* file, with the following content:
@@ -80,7 +97,7 @@ provider "aws" {
 }
 
 resource "aws_instance" "app_server" {
-  ami           = "<your-AWS-AMI-ID>"
+  ami           = "<your-EC2-AMI-ID>"
   instance_type = "t2.micro"
 
   tags = {
@@ -89,20 +106,6 @@ resource "aws_instance" "app_server" {
 }
 ```
 
-<a name="AMI-query"></a>To find `<your-AWS-AMI-ID>`, you can query the AMI Catalog via
-  a. [AWS dashboard](https://console.aws.amazon.com/ec2/v2/home?region=us-east-1#AMICatalog:), searching "Ubuntu", selecting "Quickstart AMIs" and filtering by "Free-tier",
-  b. or (preferred) with CLI, and get the 2 most recent AMIs:
-  ``` shell
-  lcl$ aws ec2 describe-images --region us-east-1 \
-    --filters "Name=root-device-type,Values=ebs" \
-              "Name=name,Values= ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server*" \
-    --query "reverse(sort_by(Images, &ImageId))[:2].[ImageId]" --output text
-  ami-0fa37863afb290840
-  ami-0e2512bd9da751ea8
-  ```
-
-:bulb: In the following task(s), we use the first AMI found.
-
 Initialize your sandbox with:
 
 ``` shell
@@ -497,10 +500,46 @@ lcl$ terraform output -json instance_tags
 instance.
 
 Did you try to SSH into the instance you created via TF? It cannot work,
-because we did not instructed TF about users, keys or anything else. **This is
-left entirely to you as an exercise.** You need to:
+because we did not instructed TF about networks, users, keys or anything
+else. **This is left entirely to you as an exercise.** You need to:
+
+  1. Destroy your infrastructure. There's a special TF command for that.
+  1. Create an SSH key pair `tf-cloud-init`.
+  1. Create a new cloud-init file
+     `~/terraform/AWS/scripts/add-ssh.yaml` with the following content:
+     ``` yaml
+     #cloud-config
+     #^^^^^^^^^^^^
+     # DO NOT TOUCH the first line!
+     ---
+     groups:
+       - ubuntu: [root, sys]
+       - terraform
+
+     users:
+       - default
+       - name: terraform
+         gecos: terraform
+         primary_group: terraform
+         groups: users, admin
+         ssh_authorized_keys:
+           - <your-SSH-pub-key-on-one-line>
+     ```
+     :warning: **Mind that the first line of this file must spell exactly
+     `#cloud-config`**!
+  1. Modify the `main.tf` as follows:
+     1. add a `resource` block of type `"aws_security_group"` allowing ingress
+        ports 22 and 80 from any address, with any egress port open;
+     1. add a `data` block referencing the above authorization file as a
+        `"template_file"` type;
+     1. extend the `"aws_instance"` resource to:
+        1. associate a public IP address,
+        1. link the `data` block to a user data attribute;
+     1. add an output `"public_ip"`.
+
+When done, *init* your new plan, *validate* and *apply* it. Verify that you
+can SSH as user `terraform` (not the customary `ubuntu`) into your instance:
 
-  0. Destroy your infrastructure. There's a special TF command for that.
-  1. Create an SSH key pair.
-  2. Extend the `main.tf` with a `data` block referencing
-  ...
+``` shell
+lcl$ ssh terraform@$(terraform output -raw public_ip) -i ../tf-cloud-init
+```
-- 
GitLab