├── .gitignore ├── .pre-commit-config.yaml ├── .talismanrc ├── README.md ├── doc ├── AWS-VPC-ASG-Nginx.png ├── AWS-VPC-FullApp-TargetGrps.png ├── AWS-VPC-FullApp.png ├── AWS-VPC-Nginx.png ├── eks.png ├── lambda-arch.png ├── stocks.png └── voteapp.png ├── exercises ├── exercise1 │ ├── README.md │ ├── main.tf │ ├── outputs.tf │ ├── terraform.tfvars │ └── variables.tf ├── exercise10 │ ├── .gitignore │ ├── README.md │ ├── __tests__ │ │ └── main-test.ts │ ├── cdktf.json │ ├── help │ ├── jest.config.js │ ├── main.ts │ ├── package-lock.json │ ├── package.json │ ├── setup.js │ └── tsconfig.json ├── exercise2 │ ├── README.md │ ├── ec2.userdata │ ├── main.tf │ ├── outputs.tf │ ├── terraform.tfvars │ └── variables.tf ├── exercise3 │ ├── README.md │ ├── ec2.userdata │ ├── main.tf │ ├── outputs.tf │ ├── terraform.tfvars │ └── variables.tf ├── exercise4 │ ├── README.md │ ├── main.tf │ ├── modules │ │ ├── application │ │ │ ├── main.tf │ │ │ ├── outputs.tf │ │ │ └── vars.tf │ │ ├── bastion │ │ │ ├── main.tf │ │ │ ├── outputs.tf │ │ │ └── vars.tf │ │ ├── network │ │ │ ├── main.tf │ │ │ ├── outputs.tf │ │ │ └── vars.tf │ │ ├── security │ │ │ ├── main.tf │ │ │ ├── outputs.tf │ │ │ └── vars.tf │ │ └── storage │ │ │ ├── install.sh │ │ │ ├── main.tf │ │ │ ├── outputs.tf │ │ │ └── vars.tf │ ├── outputs.tf │ ├── terraform.tfvars │ └── variables.tf ├── exercise5 │ ├── README.md │ ├── ansible.tf │ ├── ansible │ │ ├── ansible.cfg │ │ ├── playbooks │ │ │ ├── database.yml │ │ │ ├── deployapp.yml │ │ │ ├── files │ │ │ │ ├── api.sh │ │ │ │ ├── db.sh │ │ │ │ └── frontend.sh │ │ │ └── master.yml │ │ └── templates │ │ │ ├── hosts │ │ │ └── ssh_config │ ├── inspec │ │ └── webserver │ │ │ ├── README.md │ │ │ ├── controls │ │ │ └── nginx.rb │ │ │ └── inspec.yml │ ├── main.tf │ ├── modules │ │ ├── application │ │ │ ├── main.tf │ │ │ ├── outputs.tf │ │ │ └── vars.tf │ │ ├── bastion │ │ │ ├── main.tf │ │ │ ├── outputs.tf │ │ │ └── vars.tf │ │ ├── network │ │ │ ├── main.tf │ │ │ ├── outputs.tf │ │ │ └── vars.tf │ │ ├── security │ │ │ ├── main.tf │ │ │ ├── outputs.tf │ │ │ └── vars.tf │ │ └── storage │ │ │ ├── install.sh │ │ │ ├── main.tf │ │ │ ├── outputs.tf │ │ │ └── vars.tf │ ├── outputs.tf │ ├── terraform.tfvars │ └── variables.tf ├── exercise6 │ ├── README.md │ ├── k8s │ │ ├── app.install.sh │ │ ├── manifests │ │ │ ├── .gitignore │ │ │ └── 1_db.yaml │ │ └── templates │ │ │ ├── 2_api.yaml │ │ │ └── 3_frontend.yaml │ ├── main.tf │ └── notes ├── exercise7 │ ├── .gitignore │ ├── api_gw.tf │ ├── fns │ │ ├── bitcoin │ │ │ └── code │ │ │ │ └── lambda_function.py │ │ ├── hello │ │ │ └── code │ │ │ │ └── lambda_function.py │ │ └── pi │ │ │ └── code │ │ │ └── lambda_function.py │ ├── main.tf │ ├── modules │ │ └── lambda_function │ │ │ ├── main.tf │ │ │ ├── outputs.tf │ │ │ └── vars.tf │ ├── outputs.tf │ └── vars.tf ├── exercise8 │ ├── .gitignore │ ├── domain-join.ps1 │ ├── main.tf │ ├── modules │ │ └── ad │ │ │ ├── ad.tf │ │ │ ├── iam.tf │ │ │ ├── output.tf │ │ │ └── variables.tf │ ├── notes │ ├── outputs.tf │ └── variables.tf └── exercise9 │ ├── main.tf │ ├── modules │ └── cicd │ │ ├── iam │ │ ├── codebuild │ │ │ ├── assume_role_policy.tpl │ │ │ └── permissions_policy.tpl │ │ └── codepipeline │ │ │ ├── assume_role_policy.tpl │ │ │ └── permissions_policy.tpl │ │ ├── main.tf │ │ ├── outputs.tf │ │ └── variables.tf │ ├── notes │ ├── outputs.tf │ └── stocks │ ├── stocks-api │ ├── .gitignore │ ├── Dockerfile │ ├── buildspec.yml │ ├── pom.xml │ └── src │ │ ├── main │ │ ├── java │ │ │ └── com │ │ │ │ └── cloudacademy │ │ │ │ └── stocks │ │ │ │ ├── StockApi.java │ │ │ │ ├── controller │ │ │ │ └── StockController.java │ │ │ │ ├── entity │ │ │ │ └── Stock.java │ │ │ │ ├── repo │ │ │ │ └── StockRepository.java │ │ │ │ └── service │ │ │ │ ├── StockService.java │ │ │ │ └── StockServiceImpl.java │ │ └── resources │ │ │ └── application.properties │ │ └── test │ │ └── java │ │ └── com │ │ └── cloudacademy │ │ └── banking │ │ └── customer │ │ └── CustomerApplicationTests.java │ ├── stocks-app │ ├── .env │ ├── .gitignore │ ├── Dockerfile │ ├── buildspec.yml │ ├── conf │ │ └── conf.d │ │ │ ├── default.conf │ │ │ └── gzip.conf │ ├── data │ │ └── GOOG.csv │ ├── env-config.js │ ├── env.sh │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ └── src │ │ ├── Chart.js │ │ ├── index.js │ │ └── utils.js │ └── stocks-db │ ├── Dockerfile │ ├── buildspec.yml │ ├── create-local-db.sql │ └── data.sql └── terraform-cli-install ├── linux └── install.sh └── macos └── install.sh /.gitignore: -------------------------------------------------------------------------------- 1 | ### Terraform ### 2 | # Local .terraform directories 3 | **/.terraform/* 4 | 5 | # .tfstate files 6 | *.tfstate 7 | *.tfstate.* 8 | 9 | # Crash log files 10 | crash.log 11 | 12 | # Exclude all .tfvars files, which are likely to contain sentitive data, such as 13 | # password, private keys, and other secrets. These should not be part of version 14 | # control as they are data points which are potentially sensitive and subject 15 | # to change depending on the environment. 16 | # 17 | #*.tfvars 18 | 19 | # Ignore override files as they are usually used to override resources locally and so 20 | # are not checked in 21 | override.tf 22 | override.tf.json 23 | *_override.tf 24 | *_override.tf.json 25 | 26 | # Include override files you do wish to add to version control using negated pattern 27 | # !example_override.tf 28 | 29 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 30 | # example: *tfplan* 31 | 32 | # Ignore CLI configuration files 33 | .terraformrc 34 | terraform.rc 35 | 36 | .terraform.lock.hcl 37 | 38 | .DS_Store 39 | **/.DS_Store 40 | 41 | **/ansible/hosts 42 | **/ansible/ssh_config 43 | **/ansible/logs/ansible.log 44 | 45 | **/inspec.lock -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/antonbabenko/pre-commit-terraform 3 | rev: "v1.77.1" 4 | hooks: 5 | - id: terraform_fmt 6 | - id: terraform_tflint 7 | # - id: terraform_validate 8 | - id: terraform_tfsec 9 | 10 | - repo: https://github.com/thoughtworks/talisman 11 | rev: v1.31.0 12 | hooks: 13 | - id: talisman-commit 14 | entry: talisman --githook pre-commit -------------------------------------------------------------------------------- /.talismanrc: -------------------------------------------------------------------------------- 1 | fileignoreconfig: 2 | - filename: exercises/exercise5/ansible/playbooks/database.yml 3 | checksum: 14bbe840a5fc4d4b8b4edf50ea20916cff0224339498fa7ac9a2b4d78fd4b48b 4 | - filename: exercises/exercise5/main.tf 5 | checksum: 4cac9e387f2c8a2a78a99e4deba478371f7dde530f686a69e67836568f68a116 6 | - filename: exercises/exercise5/modules/bastion/main.tf 7 | checksum: ca67bf882bc7974d5daa787837fb12cc091586e3c1773d7407111f355a846ef9 8 | - filename: exercises/exercise5/modules/application/main.tf 9 | checksum: c0353fe90df4e1a2e9c649b466a41989d6d043fe9a4129ccc69d1e7a12ae4fec 10 | - filename: exercises/exercise5/modules/storage/main.tf 11 | checksum: ad13fff6c1201ed1ec92ebc73b20f39f55f3e2522add670e781244523d8fd520 12 | version: "" -------------------------------------------------------------------------------- /doc/AWS-VPC-ASG-Nginx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudacademy/terraform-aws/682493fbb3b5418f88cfa4ff82db1ac1e3c3b75a/doc/AWS-VPC-ASG-Nginx.png -------------------------------------------------------------------------------- /doc/AWS-VPC-FullApp-TargetGrps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudacademy/terraform-aws/682493fbb3b5418f88cfa4ff82db1ac1e3c3b75a/doc/AWS-VPC-FullApp-TargetGrps.png -------------------------------------------------------------------------------- /doc/AWS-VPC-FullApp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudacademy/terraform-aws/682493fbb3b5418f88cfa4ff82db1ac1e3c3b75a/doc/AWS-VPC-FullApp.png -------------------------------------------------------------------------------- /doc/AWS-VPC-Nginx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudacademy/terraform-aws/682493fbb3b5418f88cfa4ff82db1ac1e3c3b75a/doc/AWS-VPC-Nginx.png -------------------------------------------------------------------------------- /doc/eks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudacademy/terraform-aws/682493fbb3b5418f88cfa4ff82db1ac1e3c3b75a/doc/eks.png -------------------------------------------------------------------------------- /doc/lambda-arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudacademy/terraform-aws/682493fbb3b5418f88cfa4ff82db1ac1e3c3b75a/doc/lambda-arch.png -------------------------------------------------------------------------------- /doc/stocks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudacademy/terraform-aws/682493fbb3b5418f88cfa4ff82db1ac1e3c3b75a/doc/stocks.png -------------------------------------------------------------------------------- /doc/voteapp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudacademy/terraform-aws/682493fbb3b5418f88cfa4ff82db1ac1e3c3b75a/doc/voteapp.png -------------------------------------------------------------------------------- /exercises/exercise1/README.md: -------------------------------------------------------------------------------- 1 | ## Exercise 1 2 | Create a simple AWS VPC spanning 2 AZs. Public subnets will be created, together with an internet gateway, and single route table. A t3.micro instance will be deployed and installed with Nginx for web serving. Security groups will be created and deployed to secure all network traffic between the various components. 3 | 4 | https://github.com/cloudacademy/terraform-aws/tree/main/exercises/exercise1 5 | 6 | ![AWS Architecture](/doc/AWS-VPC-Nginx.png) 7 | 8 | #### Project Structure 9 | 10 | ``` 11 | ├── main.tf 12 | ├── outputs.tf 13 | ├── terraform.tfvars 14 | └── variables.tf 15 | ``` 16 | 17 | #### TF Variable Notes 18 | 19 | - `key_name`: The Terraform variable `key_name` represents the AWS SSH Keypair name that will be used to allow SSH access to the Bastion Host that gets created at provisioning time. If you intend to use the Bastion Host - then you will need to create your own SSH Keypair (typically done within the AWS EC2 console) ahead of time. 20 | 21 | - The required Terraform `key_name` variable can be established multiple ways, one of which is to prefix the variable name with `TF_VAR_` and have it then set as an environment variable within your shell, something like: 22 | 23 | - **Linux**: `export TF_VAR_key_name=your_ssh_key_name` 24 | 25 | - **Windows**: `set TF_VAR_key_name=your_ssh_key_name` 26 | 27 | - Terraform environment variables are documented here: 28 | [https://www.terraform.io/cli/config/environment-variables](https://www.terraform.io/cli/config/environment-variables) -------------------------------------------------------------------------------- /exercises/exercise1/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.5" 3 | required_providers { 4 | aws = { 5 | source = "hashicorp/aws" 6 | version = "~> 5.0" 7 | } 8 | } 9 | } 10 | 11 | provider "aws" { 12 | region = "us-west-2" 13 | } 14 | 15 | resource "aws_vpc" "main" { 16 | cidr_block = "10.0.0.0/16" 17 | 18 | tags = { 19 | Name = "CloudAcademy" 20 | Demo = "Terraform" 21 | } 22 | } 23 | 24 | resource "aws_subnet" "subnet1" { 25 | vpc_id = aws_vpc.main.id 26 | cidr_block = "10.0.1.0/24" 27 | availability_zone = var.availability_zones[0] 28 | 29 | tags = { 30 | Name = "Subnet1" 31 | Type = "Public" 32 | } 33 | } 34 | 35 | resource "aws_subnet" "subnet2" { 36 | vpc_id = aws_vpc.main.id 37 | cidr_block = "10.0.2.0/24" 38 | availability_zone = var.availability_zones[1] 39 | 40 | tags = { 41 | Name = "Subnet2" 42 | Type = "Public" 43 | } 44 | } 45 | 46 | resource "aws_internet_gateway" "main" { 47 | vpc_id = aws_vpc.main.id 48 | 49 | tags = { 50 | "Name" = "Main" 51 | "Owner" = "CloudAcademy" 52 | } 53 | } 54 | 55 | resource "aws_route_table" "rt1" { 56 | vpc_id = aws_vpc.main.id 57 | 58 | route { 59 | cidr_block = "0.0.0.0/0" 60 | gateway_id = aws_internet_gateway.main.id 61 | } 62 | 63 | tags = { 64 | Name = "Public" 65 | } 66 | } 67 | 68 | resource "aws_route_table_association" "rta1" { 69 | subnet_id = aws_subnet.subnet1.id 70 | route_table_id = aws_route_table.rt1.id 71 | } 72 | 73 | resource "aws_route_table_association" "rta2" { 74 | subnet_id = aws_subnet.subnet2.id 75 | route_table_id = aws_route_table.rt1.id 76 | } 77 | 78 | resource "aws_security_group" "webserver" { 79 | name = "Webserver" 80 | description = "Webserver network traffic" 81 | vpc_id = aws_vpc.main.id 82 | 83 | ingress { 84 | description = "SSH from anywhere" 85 | from_port = 22 86 | to_port = 22 87 | protocol = "tcp" 88 | cidr_blocks = [var.workstation_ip] 89 | } 90 | 91 | ingress { 92 | description = "80 from anywhere" 93 | from_port = 80 94 | to_port = 80 95 | protocol = "tcp" 96 | cidr_blocks = ["0.0.0.0/0"] 97 | } 98 | 99 | egress { 100 | from_port = 0 101 | to_port = 0 102 | protocol = "-1" 103 | cidr_blocks = ["0.0.0.0/0"] 104 | } 105 | 106 | tags = { 107 | Name = "Allow traffic" 108 | } 109 | } 110 | 111 | resource "aws_instance" "web" { 112 | ami = var.amis[var.region] 113 | instance_type = var.instance_type 114 | key_name = var.key_name 115 | subnet_id = aws_subnet.subnet1.id 116 | vpc_security_group_ids = [aws_security_group.webserver.id] 117 | 118 | associate_public_ip_address = true 119 | 120 | #userdata 121 | user_data = < { 7 | // The tests below are example tests, you can find more information at 8 | // https://cdk.tf/testing 9 | it.todo("should be tested"); 10 | 11 | // // All Unit tests test the synthesised terraform code, it does not create real-world resources 12 | // describe("Unit testing using assertions", () => { 13 | // it("should contain a resource", () => { 14 | // // import { Image,Container } from "./.gen/providers/docker" 15 | // expect( 16 | // Testing.synthScope((scope) => { 17 | // new MyApplicationsAbstraction(scope, "my-app", {}); 18 | // }) 19 | // ).toHaveResource(Container); 20 | 21 | // expect( 22 | // Testing.synthScope((scope) => { 23 | // new MyApplicationsAbstraction(scope, "my-app", {}); 24 | // }) 25 | // ).toHaveResourceWithProperties(Image, { name: "ubuntu:latest" }); 26 | // }); 27 | // }); 28 | 29 | // describe("Unit testing using snapshots", () => { 30 | // it("Tests the snapshot", () => { 31 | // const app = Testing.app(); 32 | // const stack = new TerraformStack(app, "test"); 33 | 34 | // new TestProvider(stack, "provider", { 35 | // accessKey: "1", 36 | // }); 37 | 38 | // new TestResource(stack, "test", { 39 | // name: "my-resource", 40 | // }); 41 | 42 | // expect(Testing.synth(stack)).toMatchSnapshot(); 43 | // }); 44 | 45 | // it("Tests a combination of resources", () => { 46 | // expect( 47 | // Testing.synthScope((stack) => { 48 | // new TestDataSource(stack, "test-data-source", { 49 | // name: "foo", 50 | // }); 51 | 52 | // new TestResource(stack, "test-resource", { 53 | // name: "bar", 54 | // }); 55 | // }) 56 | // ).toMatchInlineSnapshot(); 57 | // }); 58 | // }); 59 | 60 | // describe("Checking validity", () => { 61 | // it("check if the produced terraform configuration is valid", () => { 62 | // const app = Testing.app(); 63 | // const stack = new TerraformStack(app, "test"); 64 | 65 | // new TestDataSource(stack, "test-data-source", { 66 | // name: "foo", 67 | // }); 68 | 69 | // new TestResource(stack, "test-resource", { 70 | // name: "bar", 71 | // }); 72 | // expect(Testing.fullSynth(app)).toBeValidTerraform(); 73 | // }); 74 | 75 | // it("check if this can be planned", () => { 76 | // const app = Testing.app(); 77 | // const stack = new TerraformStack(app, "test"); 78 | 79 | // new TestDataSource(stack, "test-data-source", { 80 | // name: "foo", 81 | // }); 82 | 83 | // new TestResource(stack, "test-resource", { 84 | // name: "bar", 85 | // }); 86 | // expect(Testing.fullSynth(app)).toPlanSuccessfully(); 87 | // }); 88 | // }); 89 | }); 90 | -------------------------------------------------------------------------------- /exercises/exercise10/cdktf.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "typescript", 3 | "app": "npx ts-node main.ts", 4 | "projectId": "e5ec7cb5-65f9-492c-9b37-4258eeac746f", 5 | "sendCrashReports": "true", 6 | "terraformProviders": [], 7 | "terraformModules": [], 8 | "context": {} 9 | } -------------------------------------------------------------------------------- /exercises/exercise10/help: -------------------------------------------------------------------------------- 1 | ======================================================================================================== 2 | 3 | Your cdktf typescript project is ready! 4 | 5 | cat help Print this message 6 | 7 | Compile: 8 | npm run get Import/update Terraform providers and modules (you should check-in this directory) 9 | npm run compile Compile typescript code to javascript (or "npm run watch") 10 | npm run watch Watch for changes and compile typescript in the background 11 | npm run build Compile typescript 12 | 13 | Synthesize: 14 | cdktf synth [stack] Synthesize Terraform resources from stacks to cdktf.out/ (ready for 'terraform apply') 15 | 16 | Diff: 17 | cdktf diff [stack] Perform a diff (terraform plan) for the given stack 18 | 19 | Deploy: 20 | cdktf deploy [stack] Deploy the given stack 21 | 22 | Destroy: 23 | cdktf destroy [stack] Destroy the stack 24 | 25 | Test: 26 | npm run test Runs unit tests (edit __tests__/main-test.ts to add your own tests) 27 | npm run test:watch Watches the tests and reruns them on change 28 | 29 | Upgrades: 30 | npm run upgrade Upgrade cdktf modules to latest version 31 | npm run upgrade:next Upgrade cdktf modules to latest "@next" version (last commit) 32 | 33 | Use Providers: 34 | 35 | You can add prebuilt providers (if available) or locally generated ones using the add command: 36 | 37 | cdktf provider add "aws@~>3.0" null kreuzwerker/docker 38 | 39 | You can find all prebuilt providers on npm: https://www.npmjs.com/search?q=keywords:cdktf 40 | You can also install these providers directly through npm: 41 | 42 | npm install @cdktf/provider-aws 43 | npm install @cdktf/provider-google 44 | npm install @cdktf/provider-azurerm 45 | npm install @cdktf/provider-docker 46 | npm install @cdktf/provider-github 47 | npm install @cdktf/provider-null 48 | 49 | You can also build any module or provider locally. Learn more https://cdk.tf/modules-and-providers 50 | 51 | ======================================================================================================== 52 | -------------------------------------------------------------------------------- /exercises/exercise10/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | /* 3 | * For a detailed explanation regarding each configuration property, visit: 4 | * https://jestjs.io/docs/configuration 5 | */ 6 | 7 | module.exports = { 8 | 9 | // All imported modules in your tests should be mocked automatically 10 | // automock: false, 11 | 12 | // Stop running tests after `n` failures 13 | // bail: 0, 14 | 15 | // The directory where Jest should store its cached dependency information 16 | // cacheDirectory: "/private/var/folders/z_/v03l33d55fb57nrr3b1q03ch0000gq/T/jest_dz", 17 | 18 | // Automatically clear mock calls and instances between every test 19 | clearMocks: true, 20 | 21 | // Indicates whether the coverage information should be collected while executing the test 22 | // collectCoverage: false, 23 | 24 | // An array of glob patterns indicating a set of files for which coverage information should be collected 25 | // collectCoverageFrom: undefined, 26 | 27 | // The directory where Jest should output its coverage files 28 | // coverageDirectory: undefined, 29 | 30 | // An array of regexp pattern strings used to skip coverage collection 31 | // coveragePathIgnorePatterns: [ 32 | // "/node_modules/" 33 | // ], 34 | 35 | // Indicates which provider should be used to instrument code for coverage 36 | coverageProvider: "v8", 37 | 38 | // A list of reporter names that Jest uses when writing coverage reports 39 | // coverageReporters: [ 40 | // "json", 41 | // "text", 42 | // "lcov", 43 | // "clover" 44 | // ], 45 | 46 | // An object that configures minimum threshold enforcement for coverage results 47 | // coverageThreshold: undefined, 48 | 49 | // A path to a custom dependency extractor 50 | // dependencyExtractor: undefined, 51 | 52 | // Make calling deprecated APIs throw helpful error messages 53 | // errorOnDeprecated: false, 54 | 55 | // Force coverage collection from ignored files using an array of glob patterns 56 | // forceCoverageMatch: [], 57 | 58 | // A path to a module which exports an async function that is triggered once before all test suites 59 | // globalSetup: undefined, 60 | 61 | // A path to a module which exports an async function that is triggered once after all test suites 62 | // globalTeardown: undefined, 63 | 64 | // A set of global variables that need to be available in all test environments 65 | // globals: {}, 66 | 67 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 68 | // maxWorkers: "50%", 69 | 70 | // An array of directory names to be searched recursively up from the requiring module's location 71 | // moduleDirectories: [ 72 | // "node_modules" 73 | // ], 74 | 75 | // An array of file extensions your modules use 76 | moduleFileExtensions: ["ts", "js", "json", "node"], 77 | 78 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 79 | // moduleNameMapper: {}, 80 | 81 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 82 | // modulePathIgnorePatterns: [], 83 | 84 | // Activates notifications for test results 85 | // notify: false, 86 | 87 | // An enum that specifies notification mode. Requires { notify: true } 88 | // notifyMode: "failure-change", 89 | 90 | // A preset that is used as a base for Jest's configuration 91 | preset: "ts-jest", 92 | 93 | // Run tests from one or more projects 94 | // projects: undefined, 95 | 96 | // Use this configuration option to add custom reporters to Jest 97 | // reporters: undefined, 98 | 99 | // Automatically reset mock state between every test 100 | // resetMocks: false, 101 | 102 | // Reset the module registry before running each individual test 103 | // resetModules: false, 104 | 105 | // A path to a custom resolver 106 | // resolver: undefined, 107 | 108 | // Automatically restore mock state between every test 109 | // restoreMocks: false, 110 | 111 | // The root directory that Jest should scan for tests and modules within 112 | // rootDir: undefined, 113 | 114 | // A list of paths to directories that Jest should use to search for files in 115 | // roots: [ 116 | // "" 117 | // ], 118 | 119 | // Allows you to use a custom runner instead of Jest's default test runner 120 | // runner: "jest-runner", 121 | 122 | // The paths to modules that run some code to configure or set up the testing environment before each test 123 | // setupFiles: [], 124 | 125 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 126 | setupFilesAfterEnv: ["/setup.js"], 127 | 128 | // The number of seconds after which a test is considered as slow and reported as such in the results. 129 | // slowTestThreshold: 5, 130 | 131 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 132 | // snapshotSerializers: [], 133 | 134 | // The test environment that will be used for testing 135 | testEnvironment: "node", 136 | 137 | // Options that will be passed to the testEnvironment 138 | // testEnvironmentOptions: {}, 139 | 140 | // Adds a location field to test results 141 | // testLocationInResults: false, 142 | 143 | // The glob patterns Jest uses to detect test files 144 | testMatch: [ 145 | "**/__tests__/**/*.ts", 146 | "**/?(*.)+(spec|test).ts" 147 | ], 148 | 149 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 150 | testPathIgnorePatterns: ["/node_modules/", ".d.ts", ".js"], 151 | 152 | // The regexp pattern or array of patterns that Jest uses to detect test files 153 | // testRegex: [], 154 | 155 | // This option allows the use of a custom results processor 156 | // testResultsProcessor: undefined, 157 | 158 | // This option allows use of a custom test runner 159 | // testRunner: "jest-circus/runner", 160 | 161 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 162 | // testURL: "http://localhost", 163 | 164 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 165 | // timers: "real", 166 | 167 | // A map from regular expressions to paths to transformers 168 | // transform: undefined, 169 | 170 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 171 | // transformIgnorePatterns: [ 172 | // "/node_modules/", 173 | // "\\.pnp\\.[^\\/]+$" 174 | // ], 175 | 176 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 177 | // unmockedModulePathPatterns: undefined, 178 | 179 | // Indicates whether each individual test should be reported during the run 180 | // verbose: undefined, 181 | 182 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 183 | // watchPathIgnorePatterns: [], 184 | 185 | // Whether to use watchman for file crawling 186 | // watchman: true, 187 | }; 188 | -------------------------------------------------------------------------------- /exercises/exercise10/main.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { App, TerraformStack, TerraformOutput } from 'cdktf'; 3 | import { AwsProvider, AwsProviderDefaultTags } from '@cdktf/provider-aws/lib/provider'; 4 | import { Vpc } from '@cdktf/provider-aws/lib/vpc'; 5 | import { Subnet } from '@cdktf/provider-aws/lib/subnet'; 6 | import { SecurityGroup } from '@cdktf/provider-aws/lib/security-group'; 7 | import { Instance } from '@cdktf/provider-aws/lib/instance'; 8 | 9 | class CloudAcademyDevOpsStack extends TerraformStack { 10 | constructor(scope: Construct, id: string) { 11 | super(scope, id); 12 | 13 | // Global tags 14 | const tags: AwsProviderDefaultTags[] = [ 15 | { 16 | tags: { 17 | 'environment': 'cloudacademydevops', 18 | }, 19 | }, 20 | ]; 21 | 22 | // AWS Provider 23 | new AwsProvider(this, 'aws-provider', { 24 | region: 'us-east-2', 25 | defaultTags: tags 26 | }); 27 | 28 | // Create a VPC 29 | const vpc = new Vpc(this, 'cloudacademy', { 30 | cidrBlock: '10.0.0.0/16', 31 | }); 32 | 33 | // Create a subnet 34 | const subnet = new Subnet(this, 'private', { 35 | vpcId: vpc.id, 36 | cidrBlock: '10.0.0.0/24', 37 | }); 38 | 39 | // Example Security Group 40 | const securityGroup = new SecurityGroup(this, 'allow-all', { 41 | name: 'cloudacademy-allow-all', 42 | vpcId: vpc.id, 43 | description: 'Allow all security group', 44 | ingress: [ 45 | { 46 | fromPort: 0, 47 | toPort: 0, 48 | protocol: '-1', 49 | selfAttribute: true, 50 | }, 51 | ], 52 | egress: [ 53 | { 54 | fromPort: 0, 55 | toPort: 0, 56 | protocol: '-1', 57 | cidrBlocks: ['0.0.0.0/0'], 58 | }, 59 | ], 60 | tags: { 61 | Name: 'cloudacademy-allow-all', 62 | }, 63 | }); 64 | 65 | const ami = 'ami-08e6b682a466887dd'; 66 | const instanceType = 't4g.micro'; 67 | 68 | // EC2 Instance 69 | var instance1 = new Instance(this, 'cloudacademy-01', { 70 | ami: ami, 71 | instanceType: instanceType, 72 | subnetId: subnet.id, 73 | securityGroups: [securityGroup.id], 74 | }); 75 | 76 | new TerraformOutput(this, "cloudacademy-01-ip", { 77 | value: instance1.privateIp, 78 | }); 79 | } 80 | } 81 | 82 | const app = new App(); 83 | new CloudAcademyDevOpsStack(app, 'cdktf-example'); 84 | app.synth(); -------------------------------------------------------------------------------- /exercises/exercise10/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "exercise10", 3 | "version": "1.0.0", 4 | "main": "main.js", 5 | "types": "main.ts", 6 | "license": "MPL-2.0", 7 | "private": true, 8 | "scripts": { 9 | "get": "cdktf get", 10 | "build": "tsc", 11 | "synth": "cdktf synth", 12 | "compile": "tsc --pretty", 13 | "watch": "tsc -w", 14 | "test": "jest", 15 | "test:watch": "jest --watch", 16 | "upgrade": "npm i cdktf@latest cdktf-cli@latest", 17 | "upgrade:next": "npm i cdktf@next cdktf-cli@next" 18 | }, 19 | "engines": { 20 | "node": ">=16.0" 21 | }, 22 | "dependencies": { 23 | "@cdktf/provider-aws": "16.0.2", 24 | "cdktf": "^0.17.0", 25 | "constructs": "^10.2.68" 26 | }, 27 | "devDependencies": { 28 | "@types/jest": "^29.5.2", 29 | "@types/node": "^20.3.3", 30 | "jest": "^29.5.0", 31 | "ts-jest": "^29.1.1", 32 | "ts-node": "^10.9.1", 33 | "typescript": "^5.1.6" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /exercises/exercise10/setup.js: -------------------------------------------------------------------------------- 1 | const cdktf = require("cdktf"); 2 | cdktf.Testing.setupJest(); 3 | -------------------------------------------------------------------------------- /exercises/exercise10/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "declaration": true, 5 | "experimentalDecorators": true, 6 | "inlineSourceMap": true, 7 | "inlineSources": true, 8 | "lib": [ 9 | "es2018" 10 | ], 11 | "module": "CommonJS", 12 | "noEmitOnError": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "noImplicitAny": true, 15 | "noImplicitReturns": true, 16 | "noImplicitThis": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "resolveJsonModule": true, 20 | "strict": true, 21 | "strictNullChecks": true, 22 | "strictPropertyInitialization": true, 23 | "stripInternal": true, 24 | "target": "ES2018", 25 | "incremental": true, 26 | "skipLibCheck": true 27 | }, 28 | "include": [ 29 | "**/*.ts" 30 | ], 31 | "exclude": [ 32 | "node_modules", 33 | "cdktf.out" 34 | ] 35 | } -------------------------------------------------------------------------------- /exercises/exercise2/README.md: -------------------------------------------------------------------------------- 1 | ## Exercise 2 2 | Create an advanced AWS VPC spanning 2 AZs with both public and private subnets. An internet gateway and NAT gateway will be deployed into it. Public and private route tables will be established. An application load balancer (ALB) will be installed which will load balance traffic across an auto scaling group (ASG) of Nginx web servers. Security groups will be created and deployed to secure all network traffic between the various components. 3 | 4 | https://github.com/cloudacademy/terraform-aws/tree/main/exercises/exercise2 5 | 6 | ![AWS Architecture](/doc/AWS-VPC-ASG-Nginx.png) 7 | 8 | #### Project Structure 9 | 10 | ``` 11 | ├── ec2.userdata 12 | ├── main.tf 13 | ├── outputs.tf 14 | ├── terraform.tfvars 15 | └── variables.tf 16 | ``` 17 | 18 | #### TF Variable Notes 19 | 20 | - `workstation_ip`: The Terraform variable `workstation_ip` represents your workstation's external perimeter public IP address, and needs to be represented using CIDR notation. This IP address is used later on within the Terraform infrastructure provisioning process to lock down SSH access on the instance(s) (provisioned by Terraform) - this is a security safety measure to prevent anyone else from attempting SSH access. The public IP address will be different and unique for each user - the easiest way to get this address is to type "what is my ip address" in a google search. As an example response, lets say Google responded with `202.10.23.16` - then the value assigned to the Terraform `workstation_ip` variable would be `202.10.23.16/32` (note the `/32` is this case indicates that it is a single IP address). 21 | 22 | - `key_name`: The Terraform variable `key_name` represents the AWS SSH Keypair name that will be used to allow SSH access to the Bastion Host that gets created at provisioning time. If you intend to use the Bastion Host - then you will need to create your own SSH Keypair (typically done within the AWS EC2 console) ahead of time. 23 | 24 | - The required Terraform `workstation_ip` and `key_name` variables can be established multiple ways, one of which is to prefix the variable name with `TF_VAR_` and have it then set as an environment variable within your shell, something like: 25 | 26 | - **Linux**: `export TF_VAR_workstation_ip=202.10.23.16/32` and `export TF_VAR_key_name=your_ssh_key_name` 27 | 28 | - **Windows**: `set TF_VAR_workstation_ip=202.10.23.16/32` and `set TF_VAR_key_name=your_ssh_key_name` 29 | 30 | - Terraform environment variables are documented here: 31 | [https://www.terraform.io/cli/config/environment-variables](https://www.terraform.io/cli/config/environment-variables) -------------------------------------------------------------------------------- /exercises/exercise2/ec2.userdata: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | apt-get -y update 3 | apt-get -y install nginx git 4 | 5 | cd /var/www/html 6 | rm *.html 7 | git clone https://github.com/cloudacademy/webgl-globe/ . 8 | cp -a src/* . 9 | rm -rf {.git,*.md,src,conf.d,docs,Dockerfile,index.nginx-debian.html} 10 | 11 | systemctl restart nginx 12 | systemctl status nginx 13 | 14 | echo fin v1.00! -------------------------------------------------------------------------------- /exercises/exercise2/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.5.0" 3 | required_providers { 4 | aws = { 5 | source = "hashicorp/aws" 6 | version = ">= 5.0.0" 7 | } 8 | } 9 | } 10 | 11 | provider "aws" { 12 | region = "us-west-2" 13 | } 14 | 15 | data "aws_ami" "ubuntu" { 16 | most_recent = true 17 | 18 | filter { 19 | name = "name" 20 | values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"] 21 | } 22 | 23 | filter { 24 | name = "virtualization-type" 25 | values = ["hvm"] 26 | } 27 | 28 | # Canonical 29 | owners = ["099720109477"] 30 | } 31 | 32 | resource "aws_vpc" "main" { 33 | cidr_block = var.cidr_block 34 | instance_tenancy = "default" 35 | 36 | tags = { 37 | Name = "CloudAcademy" 38 | Demo = "Terraform" 39 | } 40 | } 41 | 42 | resource "aws_subnet" "subnet1" { 43 | vpc_id = aws_vpc.main.id 44 | cidr_block = cidrsubnet(var.cidr_block, 8, 1) 45 | availability_zone = var.availability_zones[0] 46 | 47 | tags = { 48 | Name = "Subnet1" 49 | Type = "Public" 50 | } 51 | } 52 | 53 | resource "aws_subnet" "subnet2" { 54 | vpc_id = aws_vpc.main.id 55 | cidr_block = cidrsubnet(var.cidr_block, 8, 2) 56 | availability_zone = var.availability_zones[1] 57 | 58 | tags = { 59 | Name = "Subnet2" 60 | Type = "Public" 61 | } 62 | } 63 | 64 | resource "aws_subnet" "subnet3" { 65 | vpc_id = aws_vpc.main.id 66 | cidr_block = cidrsubnet(var.cidr_block, 8, 3) 67 | availability_zone = var.availability_zones[0] 68 | 69 | tags = { 70 | Name = "Subnet3" 71 | Type = "Private" 72 | } 73 | } 74 | 75 | resource "aws_subnet" "subnet4" { 76 | vpc_id = aws_vpc.main.id 77 | cidr_block = cidrsubnet(var.cidr_block, 8, 4) 78 | availability_zone = var.availability_zones[1] 79 | 80 | tags = { 81 | Name = "Subnet4" 82 | Type = "Private" 83 | } 84 | } 85 | 86 | resource "aws_internet_gateway" "main" { 87 | vpc_id = aws_vpc.main.id 88 | 89 | tags = { 90 | "Name" = "Main" 91 | "Owner" = "CloudAcademy" 92 | } 93 | } 94 | 95 | resource "aws_eip" "nat" { 96 | domain = "vpc" 97 | } 98 | 99 | resource "aws_nat_gateway" "nat" { 100 | allocation_id = aws_eip.nat.id 101 | subnet_id = aws_subnet.subnet1.id 102 | 103 | tags = { 104 | Name = "NAT" 105 | } 106 | 107 | # To ensure proper ordering, it is recommended to add an explicit dependency 108 | # on the Internet Gateway for the VPC. 109 | depends_on = [aws_internet_gateway.main] 110 | } 111 | 112 | resource "aws_route_table" "rt1" { 113 | vpc_id = aws_vpc.main.id 114 | 115 | route { 116 | cidr_block = "0.0.0.0/0" 117 | gateway_id = aws_internet_gateway.main.id 118 | } 119 | 120 | tags = { 121 | Name = "Public" 122 | } 123 | } 124 | 125 | resource "aws_route_table" "rt2" { 126 | vpc_id = aws_vpc.main.id 127 | 128 | route { 129 | cidr_block = "0.0.0.0/0" 130 | gateway_id = aws_nat_gateway.nat.id 131 | } 132 | 133 | tags = { 134 | Name = "Private" 135 | } 136 | } 137 | 138 | resource "aws_route_table_association" "rta1" { 139 | subnet_id = aws_subnet.subnet1.id 140 | route_table_id = aws_route_table.rt1.id 141 | } 142 | 143 | resource "aws_route_table_association" "rta2" { 144 | subnet_id = aws_subnet.subnet2.id 145 | route_table_id = aws_route_table.rt1.id 146 | } 147 | 148 | resource "aws_route_table_association" "rta3" { 149 | subnet_id = aws_subnet.subnet3.id 150 | route_table_id = aws_route_table.rt2.id 151 | } 152 | 153 | resource "aws_route_table_association" "rta4" { 154 | subnet_id = aws_subnet.subnet4.id 155 | route_table_id = aws_route_table.rt2.id 156 | } 157 | 158 | resource "aws_security_group" "webserver" { 159 | name = "webserver" 160 | description = "webserver network traffic" 161 | vpc_id = aws_vpc.main.id 162 | 163 | dynamic "ingress" { 164 | for_each = var.webserver_sg_rules.ingress_rules 165 | content { 166 | description = ingress.value.description 167 | from_port = ingress.value.from_port 168 | to_port = ingress.value.to_port 169 | protocol = ingress.value.protocol 170 | cidr_blocks = ingress.value.cidr_blocks 171 | } 172 | } 173 | 174 | dynamic "egress" { 175 | for_each = var.webserver_sg_rules.egress_rules 176 | content { 177 | description = egress.value.description 178 | from_port = egress.value.from_port 179 | to_port = egress.value.to_port 180 | protocol = egress.value.protocol 181 | cidr_blocks = egress.value.cidr_blocks 182 | } 183 | } 184 | 185 | tags = { 186 | Name = "allow traffic" 187 | } 188 | } 189 | 190 | resource "aws_security_group" "alb" { 191 | name = "alb" 192 | description = "alb network traffic" 193 | vpc_id = aws_vpc.main.id 194 | 195 | ingress { 196 | description = "80 from anywhere" 197 | from_port = 80 198 | to_port = 80 199 | protocol = "tcp" 200 | cidr_blocks = ["0.0.0.0/0"] 201 | } 202 | 203 | egress { 204 | from_port = 0 205 | to_port = 0 206 | protocol = "-1" 207 | security_groups = [aws_security_group.webserver.id] 208 | } 209 | 210 | tags = { 211 | Name = "allow traffic" 212 | } 213 | } 214 | 215 | resource "aws_launch_template" "launchtemplate1" { 216 | name = "web" 217 | 218 | image_id = data.aws_ami.ubuntu.id 219 | instance_type = var.instance_type 220 | key_name = var.key_name 221 | vpc_security_group_ids = [aws_security_group.webserver.id] 222 | 223 | tag_specifications { 224 | resource_type = "instance" 225 | 226 | tags = { 227 | Name = "WebServer" 228 | } 229 | } 230 | 231 | user_data = filebase64("${path.module}/ec2.userdata") 232 | } 233 | 234 | resource "aws_lb" "alb1" { 235 | name = "alb1v2" 236 | internal = false 237 | load_balancer_type = "application" 238 | security_groups = [aws_security_group.alb.id] 239 | subnets = [aws_subnet.subnet1.id, aws_subnet.subnet2.id] 240 | 241 | enable_deletion_protection = false 242 | 243 | /* 244 | access_logs { 245 | bucket = aws_s3_bucket.lb_logs.bucket 246 | prefix = "test-lb" 247 | enabled = true 248 | } 249 | */ 250 | 251 | tags = { 252 | Environment = "Prod" 253 | } 254 | } 255 | 256 | resource "aws_alb_target_group" "webserver" { 257 | port = 80 258 | protocol = "HTTP" 259 | vpc_id = aws_vpc.main.id 260 | } 261 | 262 | resource "aws_alb_listener" "front_end" { 263 | load_balancer_arn = aws_lb.alb1.arn 264 | port = "80" 265 | protocol = "HTTP" 266 | 267 | default_action { 268 | type = "forward" 269 | target_group_arn = aws_alb_target_group.webserver.arn 270 | } 271 | } 272 | 273 | resource "aws_alb_listener_rule" "rule1" { 274 | listener_arn = aws_alb_listener.front_end.arn 275 | priority = 99 276 | 277 | action { 278 | type = "forward" 279 | target_group_arn = aws_alb_target_group.webserver.arn 280 | } 281 | 282 | condition { 283 | path_pattern { 284 | values = ["/"] 285 | } 286 | } 287 | } 288 | 289 | resource "aws_autoscaling_group" "asg" { 290 | vpc_zone_identifier = [aws_subnet.subnet3.id, aws_subnet.subnet4.id] 291 | 292 | desired_capacity = 2 293 | max_size = 2 294 | min_size = 2 295 | 296 | target_group_arns = [aws_alb_target_group.webserver.arn] 297 | 298 | launch_template { 299 | id = aws_launch_template.launchtemplate1.id 300 | version = "$Latest" 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /exercises/exercise2/outputs.tf: -------------------------------------------------------------------------------- 1 | output "vpc_id" { 2 | value = aws_vpc.main.id 3 | } 4 | 5 | output "subnet1_id" { 6 | value = aws_subnet.subnet1.id 7 | } 8 | 9 | output "subnet2_id" { 10 | value = aws_subnet.subnet2.id 11 | } 12 | 13 | output "subnet3_id" { 14 | value = aws_subnet.subnet3.id 15 | } 16 | 17 | output "subnet4_id" { 18 | value = aws_subnet.subnet4.id 19 | } 20 | 21 | output "alb_dns_name" { 22 | description = "alb dns" 23 | value = aws_lb.alb1.dns_name 24 | } 25 | 26 | output "web_app_wait_command" { 27 | value = "until curl -is --max-time 5 http://${aws_lb.alb1.dns_name} | grep '200 OK'; do echo preparing...; sleep 5; done; echo; echo -e 'Ready!!'" 28 | description = "Test command - tests readiness of the web app" 29 | } 30 | -------------------------------------------------------------------------------- /exercises/exercise2/terraform.tfvars: -------------------------------------------------------------------------------- 1 | availability_zones = ["us-west-2a", "us-west-2b"] 2 | cidr_block = "10.0.0.0/16" 3 | instance_type = "t3.micro" -------------------------------------------------------------------------------- /exercises/exercise2/variables.tf: -------------------------------------------------------------------------------- 1 | variable "instance_type" { 2 | type = string 3 | } 4 | variable "key_name" { 5 | type = string 6 | } 7 | 8 | variable "availability_zones" { 9 | type = list(string) 10 | } 11 | 12 | variable "cidr_block" { 13 | type = string 14 | description = "VPC cidr block. Example: 10.10.0.0/16" 15 | } 16 | 17 | variable "webserver_sg_rules" { 18 | type = object({ 19 | ingress_rules = list(object({ 20 | description = string 21 | from_port = number 22 | to_port = number 23 | protocol = string 24 | cidr_blocks = list(string) 25 | })) 26 | egress_rules = list(object({ 27 | description = string 28 | from_port = number 29 | to_port = number 30 | protocol = string 31 | cidr_blocks = list(string) 32 | })) 33 | }) 34 | default = { 35 | ingress_rules = [ 36 | { 37 | description = "SSH from management workstation" 38 | from_port = 22 39 | to_port = 22 40 | protocol = "tcp" 41 | cidr_blocks = ["1.1.1.1/32"] # <- replace with your own workstation IP 42 | }, 43 | { 44 | description = "80 from public subnets (ALB)" 45 | from_port = 80 46 | to_port = 80 47 | protocol = "tcp" 48 | cidr_blocks = ["10.0.1.0/24", "10.0.2.0/24"] 49 | }, 50 | ] 51 | egress_rules = [ 52 | { 53 | description = "All outbound internet traffic" 54 | from_port = 0 55 | to_port = 0 56 | protocol = "-1" 57 | cidr_blocks = ["0.0.0.0/0"] 58 | } 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /exercises/exercise3/README.md: -------------------------------------------------------------------------------- 1 | ## Exercise 3 2 | Same AWS architecture as used in Exercise 2. This exercise demonstrates a different Terraform technique, using the Terraform "count" meta argument, for configuring the public and private subnets as well as their respective route tables. 3 | 4 | https://github.com/cloudacademy/terraform-aws/tree/main/exercises/exercise3 5 | 6 | ![AWS Architecture](/doc/AWS-VPC-ASG-Nginx.png) 7 | 8 | #### Project Structure 9 | 10 | ``` 11 | ├── ec2.userdata 12 | ├── main.tf 13 | ├── outputs.tf 14 | ├── terraform.tfvars 15 | └── variables.tf 16 | ``` 17 | 18 | #### TF Variable Notes 19 | 20 | - `workstation_ip`: The Terraform variable `workstation_ip` represents your workstation's external perimeter public IP address, and needs to be represented using CIDR notation. This IP address is used later on within the Terraform infrastructure provisioning process to lock down SSH access on the instance(s) (provisioned by Terraform) - this is a security safety measure to prevent anyone else from attempting SSH access. The public IP address will be different and unique for each user - the easiest way to get this address is to type "what is my ip address" in a google search. As an example response, lets say Google responded with `202.10.23.16` - then the value assigned to the Terraform `workstation_ip` variable would be `202.10.23.16/32` (note the `/32` is this case indicates that it is a single IP address). 21 | 22 | - `key_name`: The Terraform variable `key_name` represents the AWS SSH Keypair name that will be used to allow SSH access to the Bastion Host that gets created at provisioning time. If you intend to use the Bastion Host - then you will need to create your own SSH Keypair (typically done within the AWS EC2 console) ahead of time. 23 | 24 | - The required Terraform `workstation_ip` and `key_name` variables can be established multiple ways, one of which is to prefix the variable name with `TF_VAR_` and have it then set as an environment variable within your shell, something like: 25 | 26 | - **Linux**: `export TF_VAR_workstation_ip=202.10.23.16/32` and `export TF_VAR_key_name=your_ssh_key_name` 27 | 28 | - **Windows**: `set TF_VAR_workstation_ip=202.10.23.16/32` and `set TF_VAR_key_name=your_ssh_key_name` 29 | 30 | - Terraform environment variables are documented here: 31 | [https://www.terraform.io/cli/config/environment-variables](https://www.terraform.io/cli/config/environment-variables) -------------------------------------------------------------------------------- /exercises/exercise3/ec2.userdata: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | apt-get -y update 3 | apt-get -y install nginx 4 | 5 | cd /var/www/html 6 | rm *.html 7 | git clone https://github.com/cloudacademy/webgl-globe/ . 8 | cp -a src/* . 9 | rm -rf {.git,*.md,src,conf.d,docs,Dockerfile,index.nginx-debian.html} 10 | 11 | systemctl restart nginx 12 | systemctl status nginx 13 | 14 | echo fin v1.00! -------------------------------------------------------------------------------- /exercises/exercise3/outputs.tf: -------------------------------------------------------------------------------- 1 | output "vpc_id" { 2 | value = aws_vpc.main.id 3 | } 4 | 5 | output "public_subnets" { 6 | value = aws_subnet.public.*.id 7 | } 8 | 9 | output "public_cidrs" { 10 | value = aws_subnet.public.*.cidr_block 11 | } 12 | 13 | output "private_subnets" { 14 | value = aws_subnet.private.*.id 15 | } 16 | 17 | output "private_cidrs" { 18 | value = aws_subnet.private.*.cidr_block 19 | } 20 | 21 | output "alb_dns_name" { 22 | description = "alb dns" 23 | value = aws_lb.alb1.dns_name 24 | } 25 | 26 | output "bastion_public_ip" { 27 | description = "bastion public ip" 28 | value = aws_instance.bastion.public_ip 29 | } 30 | 31 | output "webserver_private_ips" { 32 | description = "webserver private ips" 33 | value = data.aws_instances.webserver.private_ips 34 | } 35 | 36 | output "web_app_wait_command" { 37 | value = "until curl --max-time 5 http://${aws_lb.alb1.dns_name} >/dev/null 2>&1; do echo preparing...; sleep 5; done; echo; echo -e 'Ready!!'" 38 | description = "Test command - tests readiness of the web app" 39 | } 40 | -------------------------------------------------------------------------------- /exercises/exercise3/terraform.tfvars: -------------------------------------------------------------------------------- 1 | availability_zones = ["us-west-2a", "us-west-2b"] 2 | cidr_block = "10.0.0.0/16" 3 | instance_type = "t3.micro" -------------------------------------------------------------------------------- /exercises/exercise3/variables.tf: -------------------------------------------------------------------------------- 1 | variable "instance_type" { 2 | type = string 3 | } 4 | 5 | variable "key_name" { 6 | type = string 7 | } 8 | 9 | variable "availability_zones" { 10 | type = list(string) 11 | } 12 | 13 | variable "cidr_block" { 14 | type = string 15 | description = "VPC cidr block. Example: 10.10.0.0/16" 16 | } 17 | 18 | variable "workstation_ip" { 19 | type = string 20 | } 21 | -------------------------------------------------------------------------------- /exercises/exercise4/README.md: -------------------------------------------------------------------------------- 1 | ## Exercise 4 2 | Create an advanced AWS VPC to host a fully functioning cloud native application. 3 | 4 | ![Cloud Native Application](/doc/voteapp.png) 5 | 6 | The VPC will span 2 AZs, and have both public and private subnets. An internet gateway and NAT gateway will be deployed into it. Public and private route tables will be established. An application load balancer (ALB) will be installed which will load balance traffic across an auto scaling group (ASG) of Nginx web servers installed with the cloud native application frontend and API. A database instance running MongoDB will be installed in the private zone. Security groups will be created and deployed to secure all network traffic between the various components. 7 | 8 | For demonstration purposes only - both the frontend and the API will be deployed to the same set of ASG instances - to reduce running costs. 9 | 10 | https://github.com/cloudacademy/terraform-aws/tree/main/exercises/exercise4 11 | 12 | ![AWS Architecture](/doc/AWS-VPC-FullApp.png) 13 | 14 | The auto scaling web application layer bootstraps itself with both the [Frontend](https://github.com/cloudacademy/voteapp-frontend-react-2020) and [API](https://github.com/cloudacademy/voteapp-api-go) components by pulling down their **latest** respective releases from the following repos: 15 | 16 | * Frontend: https://github.com/cloudacademy/voteapp-frontend-react-2020/releases/latest 17 | 18 | * API: https://github.com/cloudacademy/voteapp-api-go/releases/latest 19 | 20 | The bootstrapping process for the [Frontend](https://github.com/cloudacademy/voteapp-frontend-react-2020) and [API](https://github.com/cloudacademy/voteapp-api-go) components is codified within a ```template_cloudinit_config``` block located in the application module's [main.tf](./modules/application/main.tf) file: 21 | 22 | ```terraform 23 | data "template_cloudinit_config" "config" { 24 | gzip = false 25 | base64_encode = false 26 | 27 | #userdata 28 | part { 29 | content_type = "text/x-shellscript" 30 | content = <<-EOF 31 | #! /bin/bash 32 | apt-get -y update 33 | apt-get -y install nginx 34 | apt-get -y install jq 35 | 36 | ALB_DNS=${aws_lb.alb1.dns_name} 37 | MONGODB_PRIVATEIP=${var.mongodb_ip} 38 | 39 | mkdir -p /tmp/cloudacademy-app 40 | cd /tmp/cloudacademy-app 41 | 42 | echo =========================== 43 | echo FRONTEND - download latest release and install... 44 | mkdir -p ./voteapp-frontend-react-2020 45 | pushd ./voteapp-frontend-react-2020 46 | curl -sL https://api.github.com/repos/cloudacademy/voteapp-frontend-react-2020/releases/latest | jq -r '.assets[0].browser_download_url' | xargs curl -OL 47 | INSTALL_FILENAME=$(curl -sL https://api.github.com/repos/cloudacademy/voteapp-frontend-react-2020/releases/latest | jq -r '.assets[0].name') 48 | tar -xvzf $INSTALL_FILENAME 49 | rm -rf /var/www/html 50 | cp -R build /var/www/html 51 | cat > /var/www/html/env-config.js << EOFF 52 | window._env_ = {REACT_APP_APIHOSTPORT: "$ALB_DNS"} 53 | EOFF 54 | popd 55 | 56 | echo =========================== 57 | echo API - download latest release, install, and start... 58 | mkdir -p ./voteapp-api-go 59 | pushd ./voteapp-api-go 60 | curl -sL https://api.github.com/repos/cloudacademy/voteapp-api-go/releases/latest | jq -r '.assets[] | select(.name | contains("linux-amd64")) | .browser_download_url' | xargs curl -OL 61 | INSTALL_FILENAME=$(curl -sL https://api.github.com/repos/cloudacademy/voteapp-api-go/releases/latest | jq -r '.assets[] | select(.name | contains("linux-amd64")) | .name') 62 | tar -xvzf $INSTALL_FILENAME 63 | #start the API up... 64 | MONGO_CONN_STR=mongodb://$MONGODB_PRIVATEIP:27017/langdb ./api & 65 | popd 66 | 67 | systemctl restart nginx 68 | systemctl status nginx 69 | echo fin v1.00! 70 | 71 | EOF 72 | } 73 | } 74 | ``` 75 | 76 | #### ALB Target Group Configuration 77 | 78 | The ALB will configured with a single listener (port 80). 2 target groups will be established. The frontend target group points to the Nginx web server (port 80). The API target group points to the custom API service (port 8080). 79 | 80 | ![AWS Architecture](/doc/AWS-VPC-FullApp-TargetGrps.png) 81 | 82 | #### Project Structure 83 | 84 | ``` 85 | ├── main.tf 86 | ├── modules 87 | │   ├── application 88 | │   │   ├── main.tf 89 | │   │   ├── outputs.tf 90 | │   │   └── vars.tf 91 | │   ├── bastion 92 | │   │   ├── main.tf 93 | │   │   ├── outputs.tf 94 | │   │   └── vars.tf 95 | │   ├── network 96 | │   │   ├── main.tf 97 | │   │   ├── outputs.tf 98 | │   │   └── vars.tf 99 | │   ├── security 100 | │   │   ├── main.tf 101 | │   │   ├── outputs.tf 102 | │   │   └── vars.tf 103 | │   └── storage 104 | │   ├── install.sh 105 | │   ├── main.tf 106 | │   ├── outputs.tf 107 | │   └── vars.tf 108 | ├── outputs.tf 109 | ├── terraform.tfvars 110 | └── variables.tf 111 | ``` 112 | 113 | #### TF Variable Notes 114 | 115 | - `workstation_ip`: The Terraform variable `workstation_ip` represents your workstation's external perimeter public IP address, and needs to be represented using CIDR notation. This IP address is used later on within the Terraform infrastructure provisioning process to lock down SSH access on the instance(s) (provisioned by Terraform) - this is a security safety measure to prevent anyone else from attempting SSH access. The public IP address will be different and unique for each user - the easiest way to get this address is to type "what is my ip address" in a google search. As an example response, lets say Google responded with `202.10.23.16` - then the value assigned to the Terraform `workstation_ip` variable would be `202.10.23.16/32` (note the `/32` is this case indicates that it is a single IP address). 116 | 117 | - `key_name`: The Terraform variable `key_name` represents the AWS SSH Keypair name that will be used to allow SSH access to the Bastion Host that gets created at provisioning time. If you intend to use the Bastion Host - then you will need to create your own SSH Keypair (typically done within the AWS EC2 console) ahead of time. 118 | 119 | - The required Terraform `workstation_ip` and `key_name` variables can be established multiple ways, one of which is to prefix the variable name with `TF_VAR_` and have it then set as an environment variable within your shell, something like: 120 | 121 | - **Linux**: `export TF_VAR_workstation_ip=202.10.23.16/32` and `export TF_VAR_key_name=your_ssh_key_name` 122 | 123 | - **Windows**: `set TF_VAR_workstation_ip=202.10.23.16/32` and `set TF_VAR_key_name=your_ssh_key_name` 124 | 125 | - Terraform environment variables are documented here: 126 | [https://www.terraform.io/cli/config/environment-variables](https://www.terraform.io/cli/config/environment-variables) -------------------------------------------------------------------------------- /exercises/exercise4/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.5" 3 | required_providers { 4 | aws = { 5 | source = "hashicorp/aws" 6 | version = "~> 5.0" 7 | } 8 | } 9 | } 10 | 11 | provider "aws" { 12 | region = "us-west-2" 13 | } 14 | 15 | #==================================== 16 | 17 | module "network" { 18 | source = "./modules/network" 19 | 20 | availability_zones = var.availability_zones 21 | cidr_block = var.cidr_block 22 | } 23 | 24 | #==================================== 25 | 26 | module "security" { 27 | source = "./modules/security" 28 | 29 | vpc_id = module.network.vpc_id 30 | workstation_ip = var.workstation_ip 31 | 32 | depends_on = [ 33 | module.network 34 | ] 35 | } 36 | 37 | #==================================== 38 | 39 | module "bastion" { 40 | source = "./modules/bastion" 41 | 42 | instance_type = var.bastion_instance_type 43 | key_name = var.key_name 44 | subnet_id = module.network.public_subnets[0] 45 | sg_id = module.security.bastion_sg_id 46 | 47 | depends_on = [ 48 | module.network, 49 | module.security 50 | ] 51 | } 52 | 53 | #==================================== 54 | 55 | module "storage" { 56 | source = "./modules/storage" 57 | 58 | instance_type = var.db_instance_type 59 | key_name = var.key_name 60 | subnet_id = module.network.private_subnets[0] 61 | sg_id = module.security.mongodb_sg_id 62 | 63 | depends_on = [ 64 | module.network, 65 | module.security 66 | ] 67 | } 68 | 69 | #==================================== 70 | 71 | module "application" { 72 | source = "./modules/application" 73 | 74 | instance_type = var.app_instance_type 75 | key_name = var.key_name 76 | vpc_id = module.network.vpc_id 77 | public_subnets = module.network.public_subnets 78 | private_subnets = module.network.private_subnets 79 | webserver_sg_id = module.security.application_sg_id 80 | alb_sg_id = module.security.alb_sg_id 81 | mongodb_ip = module.storage.private_ip 82 | 83 | depends_on = [ 84 | module.network, 85 | module.security, 86 | module.storage 87 | ] 88 | } 89 | 90 | -------------------------------------------------------------------------------- /exercises/exercise4/modules/application/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.4.0" 3 | required_providers { 4 | aws = { 5 | source = "hashicorp/aws" 6 | version = ">= 5.0.0" 7 | } 8 | template = { 9 | source = "hashicorp/template" 10 | version = "2.2.0" 11 | } 12 | } 13 | } 14 | 15 | data "aws_ami" "ubuntu" { 16 | most_recent = true 17 | 18 | filter { 19 | name = "name" 20 | values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"] 21 | } 22 | 23 | filter { 24 | name = "virtualization-type" 25 | values = ["hvm"] 26 | } 27 | 28 | # Canonical 29 | owners = ["099720109477"] 30 | } 31 | 32 | #==================================== 33 | 34 | data "template_cloudinit_config" "config" { 35 | gzip = false 36 | base64_encode = false 37 | 38 | #userdata 39 | part { 40 | content_type = "text/x-shellscript" 41 | content = <<-EOF 42 | #! /bin/bash 43 | apt-get -y update 44 | apt-get -y install nginx 45 | apt-get -y install jq 46 | 47 | ALB_DNS=${aws_lb.alb1.dns_name} 48 | MONGODB_PRIVATEIP=${var.mongodb_ip} 49 | 50 | mkdir -p /tmp/cloudacademy-app 51 | cd /tmp/cloudacademy-app 52 | 53 | echo =========================== 54 | echo FRONTEND - download latest release and install... 55 | mkdir -p ./voteapp-frontend-react-2023 56 | pushd ./voteapp-frontend-react-2023 57 | curl -sL https://api.github.com/repos/cloudacademy/voteapp-frontend-react-2023/releases/latest | jq -r '.assets[0].browser_download_url' | xargs curl -OL 58 | INSTALL_FILENAME=$(curl -sL https://api.github.com/repos/cloudacademy/voteapp-frontend-react-2023/releases/latest | jq -r '.assets[0].name') 59 | tar -xvzf $INSTALL_FILENAME 60 | rm -rf /var/www/html 61 | cp -R build /var/www/html 62 | cat > /var/www/html/env-config.js << EOFF 63 | window._env_ = {REACT_APP_APIHOSTPORT: "$ALB_DNS"} 64 | EOFF 65 | popd 66 | 67 | echo =========================== 68 | echo API - download latest release, install, and start... 69 | mkdir -p ./voteapp-api-go 70 | pushd ./voteapp-api-go 71 | curl -sL https://api.github.com/repos/cloudacademy/voteapp-api-go/releases/latest | jq -r '.assets[] | select(.name | contains("linux-amd64")) | .browser_download_url' | xargs curl -OL 72 | INSTALL_FILENAME=$(curl -sL https://api.github.com/repos/cloudacademy/voteapp-api-go/releases/latest | jq -r '.assets[] | select(.name | contains("linux-amd64")) | .name') 73 | tar -xvzf $INSTALL_FILENAME 74 | #start the API up... 75 | MONGO_CONN_STR=mongodb://$MONGODB_PRIVATEIP:27017/langdb ./api & 76 | popd 77 | 78 | systemctl restart nginx 79 | systemctl status nginx 80 | echo fin v1.00! 81 | 82 | EOF 83 | } 84 | } 85 | 86 | #==================================== 87 | 88 | #tfsec:ignore:aws-ec2-enforce-launch-config-http-token-imds 89 | resource "aws_launch_template" "apptemplate" { 90 | name = "application" 91 | 92 | image_id = data.aws_ami.ubuntu.id 93 | instance_type = var.instance_type 94 | key_name = var.key_name 95 | vpc_security_group_ids = [var.webserver_sg_id] 96 | 97 | tag_specifications { 98 | resource_type = "instance" 99 | 100 | tags = { 101 | Name = "FrontendApp" 102 | Owner = "CloudAcademy" 103 | } 104 | } 105 | 106 | user_data = base64encode(data.template_cloudinit_config.config.rendered) 107 | } 108 | 109 | #==================================== 110 | 111 | #tfsec:ignore:aws-elb-alb-not-public 112 | resource "aws_lb" "alb1" { 113 | name = "alb1" 114 | internal = false 115 | load_balancer_type = "application" 116 | security_groups = [var.alb_sg_id] 117 | subnets = var.public_subnets 118 | drop_invalid_header_fields = true 119 | enable_deletion_protection = false 120 | 121 | /* 122 | access_logs { 123 | bucket = aws_s3_bucket.lb_logs.bucket 124 | prefix = "test-lb" 125 | enabled = true 126 | } 127 | */ 128 | 129 | tags = { 130 | Environment = "Prod" 131 | } 132 | } 133 | 134 | resource "aws_alb_target_group" "webserver" { 135 | vpc_id = var.vpc_id 136 | port = 80 137 | protocol = "HTTP" 138 | 139 | health_check { 140 | path = "/" 141 | interval = 10 142 | healthy_threshold = 3 143 | unhealthy_threshold = 6 144 | timeout = 5 145 | } 146 | } 147 | 148 | resource "aws_alb_target_group" "api" { 149 | vpc_id = var.vpc_id 150 | port = 8080 151 | protocol = "HTTP" 152 | 153 | health_check { 154 | path = "/ok" 155 | interval = 10 156 | healthy_threshold = 3 157 | unhealthy_threshold = 6 158 | timeout = 5 159 | } 160 | } 161 | 162 | #tfsec:ignore:aws-elb-http-not-used 163 | resource "aws_alb_listener" "front_end" { 164 | load_balancer_arn = aws_lb.alb1.arn 165 | port = "80" 166 | protocol = "HTTP" 167 | 168 | default_action { 169 | type = "forward" 170 | target_group_arn = aws_alb_target_group.webserver.arn 171 | } 172 | } 173 | 174 | resource "aws_alb_listener_rule" "frontend_rule1" { 175 | listener_arn = aws_alb_listener.front_end.arn 176 | priority = 100 177 | 178 | condition { 179 | path_pattern { 180 | values = ["/"] 181 | } 182 | } 183 | 184 | action { 185 | type = "forward" 186 | target_group_arn = aws_alb_target_group.webserver.arn 187 | } 188 | } 189 | 190 | resource "aws_alb_listener_rule" "api_rule1" { 191 | listener_arn = aws_alb_listener.front_end.arn 192 | priority = 10 193 | 194 | condition { 195 | path_pattern { 196 | values = [ 197 | "/languages", 198 | "/languages/*", 199 | "/languages/*/*", 200 | "/ok" 201 | ] 202 | } 203 | } 204 | 205 | action { 206 | type = "forward" 207 | target_group_arn = aws_alb_target_group.api.arn 208 | } 209 | } 210 | 211 | #==================================== 212 | 213 | resource "aws_autoscaling_group" "asg" { 214 | vpc_zone_identifier = var.private_subnets 215 | 216 | desired_capacity = var.asg_desired 217 | max_size = var.asg_max_size 218 | min_size = var.asg_min_size 219 | 220 | target_group_arns = [aws_alb_target_group.webserver.arn, aws_alb_target_group.api.arn] 221 | 222 | launch_template { 223 | id = aws_launch_template.apptemplate.id 224 | version = "$Latest" 225 | } 226 | } 227 | 228 | data "aws_instances" "application" { 229 | instance_tags = { 230 | Name = "FrontendApp" 231 | Owner = "CloudAcademy" 232 | } 233 | 234 | instance_state_names = ["pending", "running"] 235 | 236 | depends_on = [ 237 | aws_autoscaling_group.asg 238 | ] 239 | } 240 | -------------------------------------------------------------------------------- /exercises/exercise4/modules/application/outputs.tf: -------------------------------------------------------------------------------- 1 | output "dns_name" { 2 | description = "alb dns" 3 | value = aws_lb.alb1.dns_name 4 | } 5 | 6 | output "private_ips" { 7 | description = "application instance private ips" 8 | value = data.aws_instances.application.private_ips 9 | } -------------------------------------------------------------------------------- /exercises/exercise4/modules/application/vars.tf: -------------------------------------------------------------------------------- 1 | variable "instance_type" { 2 | type = string 3 | } 4 | 5 | variable "key_name" { 6 | type = string 7 | } 8 | 9 | variable "vpc_id" { 10 | type = string 11 | } 12 | 13 | variable "public_subnets" { 14 | type = list(any) 15 | } 16 | 17 | variable "private_subnets" { 18 | type = list(any) 19 | } 20 | 21 | variable "webserver_sg_id" { 22 | type = string 23 | } 24 | 25 | variable "alb_sg_id" { 26 | type = string 27 | } 28 | 29 | variable "mongodb_ip" { 30 | type = string 31 | } 32 | 33 | variable "asg_desired" { 34 | type = number 35 | default = 2 36 | } 37 | variable "asg_max_size" { 38 | type = number 39 | default = 2 40 | } 41 | variable "asg_min_size" { 42 | type = number 43 | default = 2 44 | } 45 | -------------------------------------------------------------------------------- /exercises/exercise4/modules/bastion/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.4.0" 3 | required_providers { 4 | aws = { 5 | source = "hashicorp/aws" 6 | version = ">= 5.0.0" 7 | } 8 | } 9 | } 10 | 11 | #tfsec:ignore:aws-ec2-enforce-http-token-imds 12 | resource "aws_instance" "bastion" { 13 | ami = "ami-02868af3c3df4b3aa" 14 | instance_type = var.instance_type 15 | key_name = var.key_name 16 | subnet_id = var.subnet_id 17 | vpc_security_group_ids = [var.sg_id] 18 | associate_public_ip_address = true 19 | root_block_device { 20 | volume_size = 10 21 | encrypted = true 22 | } 23 | 24 | tags = { 25 | Name = "Bastion" 26 | Owner = "CloudAcademy" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /exercises/exercise4/modules/bastion/outputs.tf: -------------------------------------------------------------------------------- 1 | output "public_ip" { 2 | description = "public ip address" 3 | value = aws_instance.bastion.public_ip 4 | } -------------------------------------------------------------------------------- /exercises/exercise4/modules/bastion/vars.tf: -------------------------------------------------------------------------------- 1 | variable "instance_type" { 2 | type = string 3 | } 4 | 5 | variable "key_name" { 6 | type = string 7 | } 8 | 9 | variable "subnet_id" { 10 | type = string 11 | } 12 | 13 | variable "sg_id" { 14 | type = string 15 | } 16 | -------------------------------------------------------------------------------- /exercises/exercise4/modules/network/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.4.0" 3 | required_providers { 4 | aws = { 5 | source = "hashicorp/aws" 6 | version = ">= 5.0.0" 7 | } 8 | } 9 | } 10 | 11 | #tfsec:ignore:aws-ec2-require-vpc-flow-logs-for-all-vpcs 12 | #tfsec:ignore:aws-ec2-no-public-ip-subnet 13 | module "vpc" { 14 | source = "terraform-aws-modules/vpc/aws" 15 | version = "5.0.0" 16 | 17 | name = "CloudAcademy" 18 | cidr = var.cidr_block 19 | 20 | azs = var.availability_zones 21 | public_subnets = ["10.0.1.0/24", "10.0.2.0/24"] 22 | private_subnets = ["10.0.101.0/24", "10.0.102.0/24"] 23 | 24 | enable_nat_gateway = true 25 | enable_vpn_gateway = false 26 | 27 | tags = { 28 | Name = "CloudAcademy" 29 | Demo = "Terraform" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /exercises/exercise4/modules/network/outputs.tf: -------------------------------------------------------------------------------- 1 | output "vpc_id" { 2 | description = "The ID of the VPC" 3 | value = module.vpc.vpc_id 4 | } 5 | 6 | output "public_subnets" { 7 | description = "List of IDs of public subnets" 8 | value = module.vpc.public_subnets 9 | } 10 | 11 | output "private_subnets" { 12 | description = "List of IDs of private subnets" 13 | value = module.vpc.private_subnets 14 | } -------------------------------------------------------------------------------- /exercises/exercise4/modules/network/vars.tf: -------------------------------------------------------------------------------- 1 | variable "availability_zones" { 2 | type = list(any) 3 | } 4 | 5 | variable "cidr_block" { 6 | type = string 7 | } 8 | -------------------------------------------------------------------------------- /exercises/exercise4/modules/security/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.4.0" 3 | required_providers { 4 | aws = { 5 | source = "hashicorp/aws" 6 | version = ">= 5.0.0" 7 | } 8 | } 9 | } 10 | 11 | resource "aws_security_group" "bastion" { 12 | name = "bastion" 13 | description = "bastion network traffic" 14 | vpc_id = var.vpc_id 15 | 16 | ingress { 17 | description = "22 from workstation" 18 | from_port = 22 19 | to_port = 22 20 | protocol = "tcp" 21 | cidr_blocks = [var.workstation_ip] 22 | } 23 | 24 | egress { 25 | description = "all all outbound traffic" 26 | from_port = 0 27 | to_port = 0 28 | protocol = "-1" 29 | cidr_blocks = ["0.0.0.0/0"] #tfsec:ignore:aws-ec2-no-public-egress-sgr 30 | } 31 | 32 | tags = { 33 | Name = "allow traffic" 34 | } 35 | } 36 | 37 | resource "aws_security_group" "alb" { 38 | name = "alb" 39 | description = "alb network traffic" 40 | vpc_id = var.vpc_id 41 | 42 | ingress { 43 | description = "80 from anywhere" 44 | from_port = 80 45 | to_port = 80 46 | protocol = "tcp" 47 | cidr_blocks = ["0.0.0.0/0"] #tfsec:ignore:aws-vpc-no-public-ingress-sgr 48 | } 49 | 50 | egress { 51 | description = "all all outbound traffic" 52 | from_port = 0 53 | to_port = 0 54 | protocol = "-1" 55 | security_groups = [aws_security_group.application.id] 56 | } 57 | 58 | tags = { 59 | Name = "allow traffic" 60 | } 61 | } 62 | 63 | resource "aws_security_group" "application" { 64 | name = "application" 65 | description = "application network traffic" 66 | vpc_id = var.vpc_id 67 | 68 | ingress { 69 | description = "80 from alb" 70 | from_port = 80 71 | to_port = 80 72 | protocol = "tcp" 73 | cidr_blocks = ["10.0.1.0/24", "10.0.2.0/24"] 74 | #security_groups = [aws_security_group.alb.id] 75 | } 76 | 77 | ingress { 78 | description = "8080 from alb" 79 | from_port = 8080 80 | to_port = 8080 81 | protocol = "tcp" 82 | cidr_blocks = ["10.0.1.0/24", "10.0.2.0/24"] 83 | #security_groups = [aws_security_group.alb.id] 84 | } 85 | 86 | ingress { 87 | description = "22 from bastion" 88 | from_port = 22 89 | to_port = 22 90 | protocol = "tcp" 91 | security_groups = [aws_security_group.bastion.id] 92 | } 93 | 94 | egress { 95 | description = "all all outbound traffic" 96 | from_port = 0 97 | to_port = 0 98 | protocol = "-1" 99 | cidr_blocks = ["0.0.0.0/0"] #tfsec:ignore:aws-ec2-no-public-egress-sgr 100 | } 101 | 102 | tags = { 103 | Name = "application allow traffic" 104 | } 105 | } 106 | 107 | resource "aws_security_group" "mongodb" { 108 | name = "mongodb" 109 | description = "mongodb network traffic" 110 | vpc_id = var.vpc_id 111 | 112 | ingress { 113 | description = "27017 from application" 114 | from_port = 27017 115 | to_port = 27017 116 | protocol = "tcp" 117 | #cidr_blocks = ["10.0.0.0/16"] 118 | security_groups = [aws_security_group.application.id] 119 | } 120 | 121 | ingress { 122 | description = "22 from bastion" 123 | from_port = 22 124 | to_port = 22 125 | protocol = "tcp" 126 | security_groups = [aws_security_group.bastion.id] 127 | } 128 | 129 | egress { 130 | description = "all all outbound traffic" 131 | from_port = 0 132 | to_port = 0 133 | protocol = "-1" 134 | cidr_blocks = ["0.0.0.0/0"] #tfsec:ignore:aws-ec2-no-public-egress-sgr 135 | } 136 | 137 | tags = { 138 | Name = "database allow traffic" 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /exercises/exercise4/modules/security/outputs.tf: -------------------------------------------------------------------------------- 1 | output "application_sg_id" { 2 | description = "web server sg id" 3 | value = aws_security_group.application.id 4 | } 5 | 6 | output "alb_sg_id" { 7 | description = "alb sg id" 8 | value = aws_security_group.alb.id 9 | } 10 | 11 | output "mongodb_sg_id" { 12 | description = "mongodb sg id" 13 | value = aws_security_group.mongodb.id 14 | } 15 | 16 | output "bastion_sg_id" { 17 | description = "bastion sg id" 18 | value = aws_security_group.bastion.id 19 | } 20 | -------------------------------------------------------------------------------- /exercises/exercise4/modules/security/vars.tf: -------------------------------------------------------------------------------- 1 | variable "vpc_id" { 2 | type = string 3 | } 4 | 5 | variable "workstation_ip" { 6 | type = string 7 | } 8 | -------------------------------------------------------------------------------- /exercises/exercise4/modules/storage/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | wget -qO - https://www.mongodb.org/static/pgp/server-4.2.asc | sudo apt-key add - 3 | echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu bionic/mongodb-org/4.2 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-4.2.list 4 | apt-get -y update 5 | apt-get install -y mongodb-org 6 | 7 | cat > /etc/mongod.conf << EOF 8 | storage: 9 | dbPath: /var/lib/mongodb 10 | journal: 11 | enabled: true 12 | 13 | systemLog: 14 | destination: file 15 | logAppend: true 16 | path: /var/log/mongodb/mongod.log 17 | 18 | net: 19 | port: 27017 20 | bindIp: 0.0.0.0 21 | 22 | processManagement: 23 | timeZoneInfo: /usr/share/zoneinfo 24 | EOF 25 | 26 | echo starting mongo... 27 | systemctl start mongod 28 | 29 | echo preparing mongodb data population script... 30 | mkdir -p /tmp/cloudacademy-app 31 | cd /tmp/cloudacademy-app 32 | 33 | cat > db.setup.js << EOF 34 | use langdb; 35 | db.languages.insert({"name" : "csharp", "codedetail" : { "usecase" : "system, web, server-side", "rank" : 5, "compiled" : false, "homepage" : "https://dotnet.microsoft.com/learn/csharp", "download" : "https://dotnet.microsoft.com/download/", "votes" : 0}}); 36 | db.languages.insert({"name" : "python", "codedetail" : { "usecase" : "system, web, server-side", "rank" : 3, "script" : false, "homepage" : "https://www.python.org/", "download" : "https://www.python.org/downloads/", "votes" : 0}}); 37 | db.languages.insert({"name" : "javascript", "codedetail" : { "usecase" : "web, client-side", "rank" : 7, "script" : false, "homepage" : "https://en.wikipedia.org/wiki/JavaScript", "download" : "n/a", "votes" : 0}}); 38 | db.languages.insert({"name" : "go", "codedetail" : { "usecase" : "system, web, server-side", "rank" : 12, "compiled" : true, "homepage" : "https://golang.org", "download" : "https://golang.org/dl/", "votes" : 0}}); 39 | db.languages.insert({"name" : "java", "codedetail" : { "usecase" : "system, web, server-side", "rank" : 1, "compiled" : true, "homepage" : "https://www.java.com/en/", "download" : "https://www.java.com/en/download/", "votes" : 0}}); 40 | db.languages.insert({"name" : "nodejs", "codedetail" : { "usecase" : "system, web, server-side", "rank" : 20, "script" : false, "homepage" : "https://nodejs.org/en/", "download" : "https://nodejs.org/en/download/", "votes" : 0}}); 41 | db.languages.find().pretty(); 42 | EOF 43 | 44 | until mongo < db.setup.js; do sleep 5; done 45 | 46 | echo fin v1.00! 47 | -------------------------------------------------------------------------------- /exercises/exercise4/modules/storage/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.4.0" 3 | required_providers { 4 | aws = { 5 | source = "hashicorp/aws" 6 | version = ">= 5.0.0" 7 | } 8 | } 9 | } 10 | 11 | #tfsec:ignore:aws-ec2-enforce-http-token-imds 12 | resource "aws_instance" "mongo" { 13 | ami = "ami-02868af3c3df4b3aa" 14 | instance_type = var.instance_type 15 | key_name = var.key_name 16 | subnet_id = var.subnet_id 17 | vpc_security_group_ids = [var.sg_id] 18 | root_block_device { 19 | volume_size = 10 20 | encrypted = true 21 | } 22 | 23 | /* 24 | user_data = << EOF 25 | #! /bin/bash 26 | sudo apt-get update 27 | sudo apt-get install -y nginx 28 | sudo systemctl start nginx 29 | sudo systemctl enable nginx 30 | echo "

CloudAcademy 2021

" | sudo tee /var/www/html/index.html 31 | EOF 32 | */ 33 | 34 | user_data = filebase64("${path.module}/install.sh") 35 | 36 | tags = { 37 | Name = "Mongo" 38 | Owner = "CloudAcademy" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /exercises/exercise4/modules/storage/outputs.tf: -------------------------------------------------------------------------------- 1 | output "private_ip" { 2 | description = "private ip address" 3 | value = aws_instance.mongo.private_ip 4 | } -------------------------------------------------------------------------------- /exercises/exercise4/modules/storage/vars.tf: -------------------------------------------------------------------------------- 1 | variable "instance_type" { 2 | type = string 3 | } 4 | 5 | variable "key_name" { 6 | type = string 7 | } 8 | 9 | variable "subnet_id" { 10 | type = string 11 | } 12 | 13 | variable "sg_id" { 14 | type = string 15 | } 16 | -------------------------------------------------------------------------------- /exercises/exercise4/outputs.tf: -------------------------------------------------------------------------------- 1 | output "alb_dns" { 2 | description = "alb dns" 3 | value = module.application.dns_name 4 | } 5 | 6 | output "bastion_public_ip" { 7 | description = "bastion public ip" 8 | value = module.bastion.public_ip 9 | } 10 | 11 | output "application_private_ips" { 12 | description = "application instance private ips" 13 | value = module.application.private_ips 14 | } 15 | 16 | output "mongodb_private_ip" { 17 | description = "mongodb private ip" 18 | value = module.storage.private_ip 19 | } 20 | 21 | output "web_app_wait_command" { 22 | value = "until curl --max-time 5 http://${module.application.dns_name} >/dev/null 2>&1; do echo preparing...; sleep 5; done; echo; echo -e 'Ready!!'" 23 | description = "Test command - tests readiness of the web app" 24 | } 25 | -------------------------------------------------------------------------------- /exercises/exercise4/terraform.tfvars: -------------------------------------------------------------------------------- 1 | availability_zones = ["us-west-2a", "us-west-2b"] 2 | cidr_block = "10.0.0.0/16" 3 | bastion_instance_type = "t3.micro" 4 | app_instance_type = "t3.micro" 5 | db_instance_type = "t3.micro" -------------------------------------------------------------------------------- /exercises/exercise4/variables.tf: -------------------------------------------------------------------------------- 1 | variable "key_name" { 2 | type = string 3 | } 4 | 5 | variable "workstation_ip" { 6 | type = string 7 | } 8 | 9 | variable "cidr_block" { 10 | type = string 11 | description = "VPC cidr block. Example: 10.10.0.0/16" 12 | } 13 | 14 | variable "availability_zones" { 15 | type = list(any) 16 | } 17 | 18 | variable "bastion_instance_type" { 19 | type = string 20 | } 21 | 22 | variable "app_instance_type" { 23 | type = string 24 | } 25 | 26 | variable "db_instance_type" { 27 | type = string 28 | } 29 | -------------------------------------------------------------------------------- /exercises/exercise5/README.md: -------------------------------------------------------------------------------- 1 | ### Exercise 5 2 | Refactoring of the Cloud Native Application (excercise 4) to use [Ansible](https://www.ansible.com/) for configuration management. 3 | 4 | ![Cloud Native Application](/doc/voteapp.png) 5 | 6 | The VPC will span 2 AZs, and have both public and private subnets. An internet gateway and NAT gateway will be deployed into it. Public and private route tables will be established. An application load balancer (ALB) will be installed which will load balance traffic across an auto scaling group (ASG) of Nginx web servers installed with the cloud native application frontend and API. A database instance running MongoDB will be installed in the private zone. Security groups will be created and deployed to secure all network traffic between the various components. 7 | 8 | For demonstration purposes only - both the frontend and the API will be deployed to the same set of ASG instances - to reduce running costs. 9 | 10 | https://github.com/cloudacademy/terraform-aws/tree/main/exercises/exercise4 11 | 12 | ![AWS Architecture](/doc/AWS-VPC-FullApp.png) 13 | 14 | The auto scaling web application layer bootstraps itself with both the [Frontend](https://github.com/cloudacademy/voteapp-frontend-react-2020) and [API](https://github.com/cloudacademy/voteapp-api-go) components by pulling down their **latest** respective releases from the following repos: 15 | 16 | * Frontend: https://github.com/cloudacademy/voteapp-frontend-react-2020/releases/latest 17 | 18 | * API: https://github.com/cloudacademy/voteapp-api-go/releases/latest 19 | 20 | The bootstrapping process for the [Frontend](https://github.com/cloudacademy/voteapp-frontend-react-2023) and [API](https://github.com/cloudacademy/voteapp-api-go) components is now performed by Ansible. An Ansible playbook is executed from within the root [main.tf](.exercises/exercise5/main.tf) file: 21 | 22 | ```terraform 23 | resource "null_resource" "ansible" { 24 | provisioner "local-exec" { 25 | interpreter = ["/bin/bash", "-c"] 26 | working_dir = "${path.module}/ansible" 27 | command = < hosts 36 | 37 | sed \ 38 | -e 's/BASTION_IP/${module.bastion.public_ip}/g' \ 39 | -e 's/SSH_KEY_NAME/${var.key_name}/g' \ 40 | ./templates/ssh_config > ssh_config 41 | 42 | #required for macos only 43 | export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES 44 | 45 | #ANSIBLE 46 | ansible-playbook -v \ 47 | -i hosts \ 48 | --extra-vars "ALB_DNS=${module.application.dns_name}" \ 49 | --extra-vars "MONGODB_PRIVATEIP=${module.storage.private_ip}" \ 50 | ./playbooks/master.yml 51 | echo finished! 52 | EOT 53 | } 54 | 55 | depends_on = [ 56 | module.bastion, 57 | module.application 58 | ] 59 | } 60 | ``` 61 | 62 | #### ALB Target Group Configuration 63 | 64 | The ALB will configured with a single listener (port 80). 2 target groups will be established. The frontend target group points to the Nginx web server (port 80). The API target group points to the custom API service (port 8080). 65 | 66 | ![AWS Architecture](/doc/AWS-VPC-FullApp-TargetGrps.png) 67 | 68 | #### Project Structure 69 | 70 | ``` 71 | ├── main.tf 72 | ├── ansible 73 | │ ├── ansible.cfg 74 | │ ├── logs 75 | │ │ └── ansible.log 76 | │ ├── playbooks 77 | │ │ ├── database.yml 78 | │ │ ├── deployapp.yml 79 | │ │ ├── files 80 | │ │ │ ├── api.sh 81 | │ │ │ ├── db.sh 82 | │ │ │ └── frontend.sh 83 | │ │ └── master.yml 84 | │ └── templates 85 | │ ├── hosts 86 | │ └── ssh_config 87 | ├── modules 88 | │ ├── application 89 | │ │ ├── main.tf 90 | │ │ ├── outputs.tf 91 | │ │ └── vars.tf 92 | │ ├── bastion 93 | │ │ ├── main.tf 94 | │ │ ├── outputs.tf 95 | │ │ └── vars.tf 96 | │ ├── network 97 | │ │ ├── main.tf 98 | │ │ ├── outputs.tf 99 | │ │ └── vars.tf 100 | │ ├── security 101 | │ │ ├── main.tf 102 | │ │ ├── outputs.tf 103 | │ │ └── vars.tf 104 | │ └── storage 105 | │ ├── install.sh 106 | │ ├── main.tf 107 | │ ├── outputs.tf 108 | │ └── vars.tf 109 | ├── outputs.tf 110 | ├── terraform.tfvars 111 | └── variables.tf 112 | ``` 113 | 114 | #### TF Variable Notes 115 | 116 | - `workstation_ip`: The Terraform variable `workstation_ip` represents your workstation's external perimeter public IP address, and needs to be represented using CIDR notation. This IP address is used later on within the Terraform infrastructure provisioning process to lock down SSH access on the instance(s) (provisioned by Terraform) - this is a security safety measure to prevent anyone else from attempting SSH access. The public IP address will be different and unique for each user - the easiest way to get this address is to type "what is my ip address" in a google search. As an example response, lets say Google responded with `202.10.23.16` - then the value assigned to the Terraform `workstation_ip` variable would be `202.10.23.16/32` (note the `/32` is this case indicates that it is a single IP address). 117 | 118 | - `key_name`: The Terraform variable `key_name` represents the AWS SSH Keypair name that will be used to allow SSH access to the Bastion Host that gets created at provisioning time. If you intend to use the Bastion Host - then you will need to create your own SSH Keypair (typically done within the AWS EC2 console) ahead of time. 119 | 120 | - The required Terraform `workstation_ip` and `key_name` variables can be established multiple ways, one of which is to prefix the variable name with `TF_VAR_` and have it then set as an environment variable within your shell, something like: 121 | 122 | - **Linux**: `export TF_VAR_workstation_ip=202.10.23.16/32` and `export TF_VAR_key_name=your_ssh_key_name` 123 | 124 | - **Windows**: `set TF_VAR_workstation_ip=202.10.23.16/32` and `set TF_VAR_key_name=your_ssh_key_name` 125 | 126 | - Terraform environment variables are documented here: 127 | [https://www.terraform.io/cli/config/environment-variables](https://www.terraform.io/cli/config/environment-variables) -------------------------------------------------------------------------------- /exercises/exercise5/ansible.tf: -------------------------------------------------------------------------------- 1 | resource "null_resource" "ansible" { 2 | provisioner "local-exec" { 3 | interpreter = ["/bin/bash", "-c"] 4 | working_dir = "${path.module}/ansible" 5 | command = < hosts 14 | 15 | sed \ 16 | -e 's/BASTION_IP/${module.bastion.public_ip}/g' \ 17 | -e 's/SSH_KEY_NAME/${var.key_name}/g' \ 18 | ./templates/ssh_config > ssh_config 19 | 20 | #required for macos only 21 | export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES 22 | 23 | #ANSIBLE 24 | ansible-playbook -v \ 25 | -i hosts \ 26 | --extra-vars "ALB_DNS=${module.application.dns_name}" \ 27 | --extra-vars "MONGODB_PRIVATEIP=${module.storage.private_ip}" \ 28 | ./playbooks/master.yml 29 | echo finished! 30 | EOT 31 | } 32 | 33 | depends_on = [ 34 | module.bastion, 35 | module.application 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /exercises/exercise5/ansible/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | log_path = ./logs/ansible.log -------------------------------------------------------------------------------- /exercises/exercise5/ansible/playbooks/database.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: mongo 3 | become: yes 4 | become_method: sudo 5 | 6 | tasks: 7 | - name: transfer install db script 8 | copy: src=db.sh dest=/tmp mode=0777 9 | 10 | - name: execute db install script 11 | command: sh /tmp/db.sh -------------------------------------------------------------------------------- /exercises/exercise5/ansible/playbooks/deployapp.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: web 3 | become: yes 4 | become_method: sudo 5 | 6 | tasks: 7 | - name: update apt 8 | apt: 9 | update_cache: true 10 | cache_valid_time: 3600 11 | force_apt_get: true 12 | 13 | - name: install jq 14 | apt: 15 | name: jq 16 | state: present 17 | 18 | - name: ensure nginx is at the latest version 19 | apt: name=nginx state=latest 20 | - name: start nginx 21 | service: 22 | name: nginx 23 | state: started 24 | 25 | - name: transfer install api script 26 | copy: src=api.sh dest=/tmp mode=0777 27 | 28 | - name: execute api install script 29 | command: sh /tmp/api.sh 30 | 31 | - name: start api service in background 32 | shell: MONGO_CONN_STR=mongodb://{{ MONGODB_PRIVATEIP }}:27017/langdb nohup /tmp/cloudacademy-app/voteapp-api-go/api /dev/null 2>&1 & 33 | 34 | - name: transfer install frontend script 35 | copy: src=frontend.sh dest=/tmp mode=0777 36 | 37 | - name: execute frontend install script 38 | command: sh /tmp/frontend.sh {{ ALB_DNS }} -------------------------------------------------------------------------------- /exercises/exercise5/ansible/playbooks/files/api.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | apt-get update 3 | echo API - download latest release, install, and start... 4 | mkdir -p /tmp/cloudacademy-app/voteapp-api-go 5 | cd /tmp/cloudacademy-app/voteapp-api-go 6 | curl -sL https://api.github.com/repos/cloudacademy/voteapp-api-go/releases/latest | jq -r '.assets[] | select(.name | contains("linux-amd64")) | .browser_download_url' | xargs curl -OL 7 | INSTALL_FILENAME=$(curl -sL https://api.github.com/repos/cloudacademy/voteapp-api-go/releases/latest | jq -r '.assets[] | select(.name | contains("linux-amd64")) | .name') 8 | tar -xvzf $INSTALL_FILENAME -------------------------------------------------------------------------------- /exercises/exercise5/ansible/playbooks/files/db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | wget -qO - https://www.mongodb.org/static/pgp/server-4.2.asc | sudo apt-key add - 3 | echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu bionic/mongodb-org/4.2 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-4.2.list 4 | apt-get update 5 | apt-get install -y mongodb-org 6 | 7 | cat > /etc/mongod.conf << EOF 8 | storage: 9 | dbPath: /var/lib/mongodb 10 | journal: 11 | enabled: true 12 | 13 | systemLog: 14 | destination: file 15 | logAppend: true 16 | path: /var/log/mongodb/mongod.log 17 | 18 | net: 19 | port: 27017 20 | bindIp: 0.0.0.0 21 | 22 | processManagement: 23 | timeZoneInfo: /usr/share/zoneinfo 24 | EOF 25 | 26 | echo starting mongo... 27 | systemctl start mongod 28 | 29 | echo preparing mongodb data population script... 30 | mkdir -p /tmp/cloudacademy-app 31 | cd /tmp/cloudacademy-app 32 | 33 | cat > db.setup.js << EOF 34 | use langdb; 35 | db.languages.insert({"name" : "csharp", "codedetail" : { "usecase" : "system, web, server-side", "rank" : 5, "compiled" : false, "homepage" : "https://dotnet.microsoft.com/learn/csharp", "download" : "https://dotnet.microsoft.com/download/", "votes" : 0}}); 36 | db.languages.insert({"name" : "python", "codedetail" : { "usecase" : "system, web, server-side", "rank" : 3, "script" : false, "homepage" : "https://www.python.org/", "download" : "https://www.python.org/downloads/", "votes" : 0}}); 37 | db.languages.insert({"name" : "javascript", "codedetail" : { "usecase" : "web, client-side", "rank" : 7, "script" : false, "homepage" : "https://en.wikipedia.org/wiki/JavaScript", "download" : "n/a", "votes" : 0}}); 38 | db.languages.insert({"name" : "go", "codedetail" : { "usecase" : "system, web, server-side", "rank" : 12, "compiled" : true, "homepage" : "https://golang.org", "download" : "https://golang.org/dl/", "votes" : 0}}); 39 | db.languages.insert({"name" : "java", "codedetail" : { "usecase" : "system, web, server-side", "rank" : 1, "compiled" : true, "homepage" : "https://www.java.com/en/", "download" : "https://www.java.com/en/download/", "votes" : 0}}); 40 | db.languages.insert({"name" : "nodejs", "codedetail" : { "usecase" : "system, web, server-side", "rank" : 20, "script" : false, "homepage" : "https://nodejs.org/en/", "download" : "https://nodejs.org/en/download/", "votes" : 0}}); 41 | db.languages.find().pretty(); 42 | EOF 43 | 44 | until mongo < db.setup.js; do sleep 5; done 45 | 46 | echo fin v1.00! 47 | -------------------------------------------------------------------------------- /exercises/exercise5/ansible/playbooks/files/frontend.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ALB_DNS=$1 3 | echo FRONTEND - download latest release and install... 4 | mkdir -p /tmp/cloudacademy-app/voteapp-frontend-react-2023 5 | cd /tmp/cloudacademy-app/voteapp-frontend-react-2023 6 | curl -sL https://api.github.com/repos/cloudacademy/voteapp-frontend-react-2023/releases/latest | jq -r '.assets[0].browser_download_url' | xargs curl -OL 7 | INSTALL_FILENAME=$(curl -sL https://api.github.com/repos/cloudacademy/voteapp-frontend-react-2023/releases/latest | jq -r '.assets[0].name') 8 | tar -xvzf $INSTALL_FILENAME 9 | rm -rf /var/www/html 10 | cp -R build /var/www/html 11 | cat > /var/www/html/env-config.js << EOFF 12 | window._env_ = {REACT_APP_APIHOSTPORT: "$ALB_DNS"} 13 | EOFF 14 | -------------------------------------------------------------------------------- /exercises/exercise5/ansible/playbooks/master.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - import_playbook: database.yml 3 | - import_playbook: deployapp.yml -------------------------------------------------------------------------------- /exercises/exercise5/ansible/templates/hosts: -------------------------------------------------------------------------------- 1 | [bastion] 2 | BASTION_IP 3 | 4 | [web] 5 | WEB_IPS 6 | 7 | [web:vars] 8 | ansible_ssh_common_args = '-F ./ssh_config' 9 | 10 | [mongo] 11 | MONGO_IP 12 | 13 | [mongo:vars] 14 | ansible_ssh_common_args = '-F ./ssh_config' 15 | -------------------------------------------------------------------------------- /exercises/exercise5/ansible/templates/ssh_config: -------------------------------------------------------------------------------- 1 | Host 10.0.* !BASTION_IP 2 | ProxyJump BASTION_IP 3 | Host 10.0.* 4 | User ubuntu 5 | StrictHostKeyChecking no 6 | UserKnownHostsFile /dev/null 7 | IdentityFile ~/.ssh/SSH_KEY_NAME.pem 8 | Host BASTION_IP 9 | User ubuntu 10 | StrictHostKeyChecking no 11 | UserKnownHostsFile /dev/null 12 | IdentityFile ~/.ssh/SSH_KEY_NAME.pem -------------------------------------------------------------------------------- /exercises/exercise5/inspec/webserver/README.md: -------------------------------------------------------------------------------- 1 | # Example InSpec Profile 2 | 3 | This example shows the implementation of an InSpec profile. 4 | -------------------------------------------------------------------------------- /exercises/exercise5/inspec/webserver/inspec.yml: -------------------------------------------------------------------------------- 1 | name: webserver 2 | title: InSpec Profile 3 | maintainer: The Authors 4 | copyright: The Authors 5 | copyright_email: you@example.com 6 | license: Apache-2.0 7 | summary: An InSpec Compliance Profile 8 | version: 0.1.0 9 | supports: 10 | platform: os -------------------------------------------------------------------------------- /exercises/exercise5/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.4.0" 3 | required_providers { 4 | aws = { 5 | source = "hashicorp/aws" 6 | version = ">= 5.0.0" 7 | } 8 | null = { 9 | source = "hashicorp/null" 10 | version = "3.2.1" 11 | } 12 | } 13 | } 14 | 15 | provider "aws" { 16 | region = "us-west-2" 17 | } 18 | 19 | provider "null" { 20 | # Configuration options 21 | } 22 | 23 | #==================================== 24 | 25 | module "network" { 26 | source = "./modules/network" 27 | 28 | availability_zones = var.availability_zones 29 | cidr_block = var.cidr_block 30 | } 31 | 32 | #==================================== 33 | 34 | module "security" { 35 | source = "./modules/security" 36 | 37 | vpc_id = module.network.vpc_id 38 | workstation_ip = var.workstation_ip 39 | 40 | depends_on = [ 41 | module.network 42 | ] 43 | } 44 | 45 | #==================================== 46 | 47 | module "bastion" { 48 | source = "./modules/bastion" 49 | 50 | instance_type = var.bastion_instance_type 51 | key_name = var.key_name 52 | subnet_id = module.network.public_subnets[0] 53 | sg_id = module.security.bastion_sg_id 54 | 55 | depends_on = [ 56 | module.network, 57 | module.security 58 | ] 59 | } 60 | 61 | #==================================== 62 | 63 | module "storage" { 64 | source = "./modules/storage" 65 | 66 | instance_type = var.db_instance_type 67 | key_name = var.key_name 68 | subnet_id = module.network.private_subnets[0] 69 | sg_id = module.security.mongodb_sg_id 70 | 71 | depends_on = [ 72 | module.network, 73 | module.security 74 | ] 75 | } 76 | 77 | #==================================== 78 | 79 | module "application" { 80 | source = "./modules/application" 81 | 82 | instance_type = var.app_instance_type 83 | key_name = var.key_name 84 | vpc_id = module.network.vpc_id 85 | public_subnets = module.network.public_subnets 86 | private_subnets = module.network.private_subnets 87 | webserver_sg_id = module.security.application_sg_id 88 | alb_sg_id = module.security.alb_sg_id 89 | 90 | depends_on = [ 91 | module.network, 92 | module.security, 93 | module.storage 94 | ] 95 | } 96 | -------------------------------------------------------------------------------- /exercises/exercise5/modules/application/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.4.0" 3 | required_providers { 4 | aws = { 5 | source = "hashicorp/aws" 6 | version = ">= 5.0.0" 7 | } 8 | template = { 9 | source = "hashicorp/template" 10 | version = "2.2.0" 11 | } 12 | } 13 | } 14 | 15 | data "aws_ami" "ubuntu" { 16 | most_recent = true 17 | 18 | filter { 19 | name = "name" 20 | values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"] 21 | } 22 | 23 | filter { 24 | name = "virtualization-type" 25 | values = ["hvm"] 26 | } 27 | 28 | # Canonical 29 | owners = ["099720109477"] 30 | } 31 | 32 | #==================================== 33 | 34 | # data "template_cloudinit_config" "config" { 35 | # gzip = false 36 | # base64_encode = false 37 | 38 | # #userdata 39 | # part { 40 | # content_type = "text/x-shellscript" 41 | # content = <<-EOF 42 | # #! /bin/bash 43 | 44 | # #ansible now performs install 45 | # echo fin v1.00! 46 | 47 | # EOF 48 | # } 49 | # } 50 | 51 | #==================================== 52 | 53 | #tfsec:ignore:aws-ec2-enforce-launch-config-http-token-imds 54 | resource "aws_launch_template" "apptemplate" { 55 | name = "application" 56 | 57 | image_id = data.aws_ami.ubuntu.id 58 | instance_type = var.instance_type 59 | key_name = var.key_name 60 | vpc_security_group_ids = [var.webserver_sg_id] 61 | 62 | tag_specifications { 63 | resource_type = "instance" 64 | 65 | tags = { 66 | Name = "FrontendApp" 67 | Owner = "CloudAcademy" 68 | } 69 | } 70 | 71 | #user_data = base64encode(data.template_cloudinit_config.config.rendered) 72 | } 73 | 74 | #==================================== 75 | 76 | #tfsec:ignore:aws-elb-alb-not-public 77 | resource "aws_lb" "alb1" { 78 | name = "alb1" 79 | internal = false 80 | load_balancer_type = "application" 81 | security_groups = [var.alb_sg_id] 82 | subnets = var.public_subnets 83 | drop_invalid_header_fields = true 84 | enable_deletion_protection = false 85 | 86 | /* 87 | access_logs { 88 | bucket = aws_s3_bucket.lb_logs.bucket 89 | prefix = "test-lb" 90 | enabled = true 91 | } 92 | */ 93 | 94 | tags = { 95 | Environment = "Prod" 96 | } 97 | } 98 | 99 | resource "aws_alb_target_group" "webserver" { 100 | vpc_id = var.vpc_id 101 | port = 80 102 | protocol = "HTTP" 103 | 104 | health_check { 105 | path = "/" 106 | interval = 10 107 | healthy_threshold = 3 108 | unhealthy_threshold = 6 109 | timeout = 5 110 | } 111 | } 112 | 113 | resource "aws_alb_target_group" "api" { 114 | vpc_id = var.vpc_id 115 | port = 8080 116 | protocol = "HTTP" 117 | 118 | health_check { 119 | path = "/ok" 120 | interval = 10 121 | healthy_threshold = 3 122 | unhealthy_threshold = 6 123 | timeout = 5 124 | } 125 | } 126 | 127 | #tfsec:ignore:aws-elb-http-not-used 128 | resource "aws_alb_listener" "front_end" { 129 | load_balancer_arn = aws_lb.alb1.arn 130 | port = "80" 131 | protocol = "HTTP" 132 | 133 | default_action { 134 | type = "forward" 135 | target_group_arn = aws_alb_target_group.webserver.arn 136 | } 137 | } 138 | 139 | resource "aws_alb_listener_rule" "frontend_rule1" { 140 | listener_arn = aws_alb_listener.front_end.arn 141 | priority = 100 142 | 143 | condition { 144 | path_pattern { 145 | values = ["/"] 146 | } 147 | } 148 | 149 | action { 150 | type = "forward" 151 | target_group_arn = aws_alb_target_group.webserver.arn 152 | } 153 | } 154 | 155 | resource "aws_alb_listener_rule" "api_rule1" { 156 | listener_arn = aws_alb_listener.front_end.arn 157 | priority = 10 158 | 159 | condition { 160 | path_pattern { 161 | values = [ 162 | "/languages", 163 | "/languages/*", 164 | "/languages/*/*", 165 | "/ok" 166 | ] 167 | } 168 | } 169 | 170 | action { 171 | type = "forward" 172 | target_group_arn = aws_alb_target_group.api.arn 173 | } 174 | } 175 | 176 | #==================================== 177 | 178 | resource "aws_autoscaling_group" "asg" { 179 | vpc_zone_identifier = var.private_subnets 180 | 181 | desired_capacity = var.asg_desired 182 | max_size = var.asg_max_size 183 | min_size = var.asg_min_size 184 | 185 | target_group_arns = [aws_alb_target_group.webserver.arn, aws_alb_target_group.api.arn] 186 | 187 | launch_template { 188 | id = aws_launch_template.apptemplate.id 189 | version = "$Latest" 190 | } 191 | } 192 | 193 | data "aws_instances" "application" { 194 | instance_tags = { 195 | Name = "FrontendApp" 196 | Owner = "CloudAcademy" 197 | } 198 | 199 | instance_state_names = ["pending", "running"] 200 | 201 | depends_on = [ 202 | aws_autoscaling_group.asg 203 | ] 204 | } 205 | -------------------------------------------------------------------------------- /exercises/exercise5/modules/application/outputs.tf: -------------------------------------------------------------------------------- 1 | output "dns_name" { 2 | description = "alb dns" 3 | value = aws_lb.alb1.dns_name 4 | } 5 | 6 | output "private_ips" { 7 | description = "application instance private ips" 8 | value = data.aws_instances.application.private_ips 9 | } -------------------------------------------------------------------------------- /exercises/exercise5/modules/application/vars.tf: -------------------------------------------------------------------------------- 1 | variable "instance_type" { 2 | type = string 3 | } 4 | 5 | variable "key_name" { 6 | type = string 7 | } 8 | 9 | variable "vpc_id" { 10 | type = string 11 | } 12 | 13 | variable "public_subnets" { 14 | type = list(any) 15 | } 16 | 17 | variable "private_subnets" { 18 | type = list(any) 19 | } 20 | 21 | variable "webserver_sg_id" { 22 | type = string 23 | } 24 | 25 | variable "alb_sg_id" { 26 | type = string 27 | } 28 | 29 | variable "asg_desired" { 30 | type = number 31 | default = 2 32 | } 33 | 34 | variable "asg_max_size" { 35 | type = number 36 | default = 2 37 | } 38 | 39 | variable "asg_min_size" { 40 | type = number 41 | default = 2 42 | } 43 | -------------------------------------------------------------------------------- /exercises/exercise5/modules/bastion/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.4.0" 3 | required_providers { 4 | aws = { 5 | source = "hashicorp/aws" 6 | version = ">= 5.0.0" 7 | } 8 | } 9 | } 10 | 11 | #tfsec:ignore:aws-ec2-enforce-http-token-imds 12 | resource "aws_instance" "bastion" { 13 | ami = "ami-02868af3c3df4b3aa" 14 | instance_type = var.instance_type 15 | key_name = var.key_name 16 | subnet_id = var.subnet_id 17 | vpc_security_group_ids = [var.sg_id] 18 | associate_public_ip_address = true 19 | root_block_device { 20 | volume_size = 10 21 | encrypted = true 22 | } 23 | 24 | tags = { 25 | Name = "Bastion" 26 | Owner = "CloudAcademy" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /exercises/exercise5/modules/bastion/outputs.tf: -------------------------------------------------------------------------------- 1 | output "public_ip" { 2 | description = "public ip address" 3 | value = aws_instance.bastion.public_ip 4 | } -------------------------------------------------------------------------------- /exercises/exercise5/modules/bastion/vars.tf: -------------------------------------------------------------------------------- 1 | variable "instance_type" { 2 | type = string 3 | } 4 | 5 | variable "key_name" { 6 | type = string 7 | } 8 | 9 | variable "subnet_id" { 10 | type = string 11 | } 12 | 13 | variable "sg_id" { 14 | type = string 15 | } 16 | -------------------------------------------------------------------------------- /exercises/exercise5/modules/network/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.4.0" 3 | required_providers { 4 | aws = { 5 | source = "hashicorp/aws" 6 | version = ">= 5.0.0" 7 | } 8 | } 9 | } 10 | 11 | #tfsec:ignore:aws-ec2-require-vpc-flow-logs-for-all-vpcs 12 | #tfsec:ignore:aws-ec2-no-public-ip-subnet 13 | module "vpc" { 14 | source = "terraform-aws-modules/vpc/aws" 15 | version = "5.0.0" 16 | 17 | name = "CloudAcademy" 18 | cidr = var.cidr_block 19 | 20 | azs = var.availability_zones 21 | public_subnets = ["10.0.1.0/24", "10.0.2.0/24"] 22 | private_subnets = ["10.0.101.0/24", "10.0.102.0/24"] 23 | 24 | enable_nat_gateway = true 25 | enable_vpn_gateway = false 26 | 27 | tags = { 28 | Name = "CloudAcademy" 29 | Demo = "Terraform" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /exercises/exercise5/modules/network/outputs.tf: -------------------------------------------------------------------------------- 1 | output "vpc_id" { 2 | description = "The ID of the VPC" 3 | value = module.vpc.vpc_id 4 | } 5 | 6 | output "public_subnets" { 7 | description = "List of IDs of public subnets" 8 | value = module.vpc.public_subnets 9 | } 10 | 11 | output "private_subnets" { 12 | description = "List of IDs of private subnets" 13 | value = module.vpc.private_subnets 14 | } -------------------------------------------------------------------------------- /exercises/exercise5/modules/network/vars.tf: -------------------------------------------------------------------------------- 1 | variable "availability_zones" { 2 | type = list(any) 3 | } 4 | 5 | variable "cidr_block" { 6 | type = string 7 | } 8 | -------------------------------------------------------------------------------- /exercises/exercise5/modules/security/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.4.0" 3 | required_providers { 4 | aws = { 5 | source = "hashicorp/aws" 6 | version = ">= 5.0.0" 7 | } 8 | } 9 | } 10 | 11 | resource "aws_security_group" "bastion" { 12 | name = "bastion" 13 | description = "bastion network traffic" 14 | vpc_id = var.vpc_id 15 | 16 | ingress { 17 | description = "22 from workstation" 18 | from_port = 22 19 | to_port = 22 20 | protocol = "tcp" 21 | cidr_blocks = [var.workstation_ip] 22 | } 23 | 24 | egress { 25 | description = "all all outbound traffic" 26 | from_port = 0 27 | to_port = 0 28 | protocol = "-1" 29 | cidr_blocks = ["0.0.0.0/0"] #tfsec:ignore:aws-ec2-no-public-egress-sgr 30 | } 31 | 32 | tags = { 33 | Name = "allow traffic" 34 | } 35 | } 36 | 37 | resource "aws_security_group" "alb" { 38 | name = "alb" 39 | description = "alb network traffic" 40 | vpc_id = var.vpc_id 41 | 42 | ingress { 43 | description = "80 from anywhere" 44 | from_port = 80 45 | to_port = 80 46 | protocol = "tcp" 47 | cidr_blocks = ["0.0.0.0/0"] #tfsec:ignore:aws-vpc-no-public-ingress-sgr 48 | } 49 | 50 | egress { 51 | description = "all all outbound traffic" 52 | from_port = 0 53 | to_port = 0 54 | protocol = "-1" 55 | security_groups = [aws_security_group.application.id] 56 | } 57 | 58 | tags = { 59 | Name = "allow traffic" 60 | } 61 | } 62 | 63 | resource "aws_security_group" "application" { 64 | name = "application" 65 | description = "application network traffic" 66 | vpc_id = var.vpc_id 67 | 68 | ingress { 69 | description = "80 from alb" 70 | from_port = 80 71 | to_port = 80 72 | protocol = "tcp" 73 | cidr_blocks = ["10.0.1.0/24", "10.0.2.0/24"] 74 | #security_groups = [aws_security_group.alb.id] 75 | } 76 | 77 | ingress { 78 | description = "8080 from alb" 79 | from_port = 8080 80 | to_port = 8080 81 | protocol = "tcp" 82 | cidr_blocks = ["10.0.1.0/24", "10.0.2.0/24"] 83 | #security_groups = [aws_security_group.alb.id] 84 | } 85 | 86 | ingress { 87 | description = "22 from bastion" 88 | from_port = 22 89 | to_port = 22 90 | protocol = "tcp" 91 | security_groups = [aws_security_group.bastion.id] 92 | } 93 | 94 | egress { 95 | description = "all all outbound traffic" 96 | from_port = 0 97 | to_port = 0 98 | protocol = "-1" 99 | cidr_blocks = ["0.0.0.0/0"] #tfsec:ignore:aws-ec2-no-public-egress-sgr 100 | } 101 | 102 | tags = { 103 | Name = "application allow traffic" 104 | } 105 | } 106 | 107 | resource "aws_security_group" "mongodb" { 108 | name = "mongodb" 109 | description = "mongodb network traffic" 110 | vpc_id = var.vpc_id 111 | 112 | ingress { 113 | description = "27017 from application" 114 | from_port = 27017 115 | to_port = 27017 116 | protocol = "tcp" 117 | #cidr_blocks = ["10.0.0.0/16"] 118 | security_groups = [aws_security_group.application.id] 119 | } 120 | 121 | ingress { 122 | description = "22 from bastion" 123 | from_port = 22 124 | to_port = 22 125 | protocol = "tcp" 126 | security_groups = [aws_security_group.bastion.id] 127 | } 128 | 129 | egress { 130 | description = "all all outbound traffic" 131 | from_port = 0 132 | to_port = 0 133 | protocol = "-1" 134 | cidr_blocks = ["0.0.0.0/0"] #tfsec:ignore:aws-ec2-no-public-egress-sgr 135 | } 136 | 137 | tags = { 138 | Name = "database allow traffic" 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /exercises/exercise5/modules/security/outputs.tf: -------------------------------------------------------------------------------- 1 | output "application_sg_id" { 2 | description = "web server sg id" 3 | value = aws_security_group.application.id 4 | } 5 | 6 | output "alb_sg_id" { 7 | description = "alb sg id" 8 | value = aws_security_group.alb.id 9 | } 10 | 11 | output "mongodb_sg_id" { 12 | description = "mongodb sg id" 13 | value = aws_security_group.mongodb.id 14 | } 15 | 16 | output "bastion_sg_id" { 17 | description = "bastion sg id" 18 | value = aws_security_group.bastion.id 19 | } 20 | -------------------------------------------------------------------------------- /exercises/exercise5/modules/security/vars.tf: -------------------------------------------------------------------------------- 1 | variable "vpc_id" { 2 | type = string 3 | } 4 | 5 | variable "workstation_ip" { 6 | type = string 7 | } 8 | -------------------------------------------------------------------------------- /exercises/exercise5/modules/storage/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | wget -qO - https://www.mongodb.org/static/pgp/server-4.2.asc | sudo apt-key add - 3 | echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu bionic/mongodb-org/4.2 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-4.2.list 4 | apt-get -y update 5 | apt-get install -y mongodb-org 6 | 7 | cat > /etc/mongod.conf << EOF 8 | storage: 9 | dbPath: /var/lib/mongodb 10 | journal: 11 | enabled: true 12 | 13 | systemLog: 14 | destination: file 15 | logAppend: true 16 | path: /var/log/mongodb/mongod.log 17 | 18 | net: 19 | port: 27017 20 | bindIp: 0.0.0.0 21 | 22 | processManagement: 23 | timeZoneInfo: /usr/share/zoneinfo 24 | EOF 25 | 26 | echo starting mongo... 27 | systemctl start mongod 28 | 29 | echo preparing mongodb data population script... 30 | mkdir -p /tmp/cloudacademy-app 31 | cd /tmp/cloudacademy-app 32 | 33 | cat > db.setup.js << EOF 34 | use langdb; 35 | db.languages.insert({"name" : "csharp", "codedetail" : { "usecase" : "system, web, server-side", "rank" : 5, "compiled" : false, "homepage" : "https://dotnet.microsoft.com/learn/csharp", "download" : "https://dotnet.microsoft.com/download/", "votes" : 0}}); 36 | db.languages.insert({"name" : "python", "codedetail" : { "usecase" : "system, web, server-side", "rank" : 3, "script" : false, "homepage" : "https://www.python.org/", "download" : "https://www.python.org/downloads/", "votes" : 0}}); 37 | db.languages.insert({"name" : "javascript", "codedetail" : { "usecase" : "web, client-side", "rank" : 7, "script" : false, "homepage" : "https://en.wikipedia.org/wiki/JavaScript", "download" : "n/a", "votes" : 0}}); 38 | db.languages.insert({"name" : "go", "codedetail" : { "usecase" : "system, web, server-side", "rank" : 12, "compiled" : true, "homepage" : "https://golang.org", "download" : "https://golang.org/dl/", "votes" : 0}}); 39 | db.languages.insert({"name" : "java", "codedetail" : { "usecase" : "system, web, server-side", "rank" : 1, "compiled" : true, "homepage" : "https://www.java.com/en/", "download" : "https://www.java.com/en/download/", "votes" : 0}}); 40 | db.languages.insert({"name" : "nodejs", "codedetail" : { "usecase" : "system, web, server-side", "rank" : 20, "script" : false, "homepage" : "https://nodejs.org/en/", "download" : "https://nodejs.org/en/download/", "votes" : 0}}); 41 | db.languages.find().pretty(); 42 | EOF 43 | 44 | until mongo < db.setup.js; do sleep 5; done 45 | 46 | echo fin v1.00! 47 | -------------------------------------------------------------------------------- /exercises/exercise5/modules/storage/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.4.0" 3 | required_providers { 4 | aws = { 5 | source = "hashicorp/aws" 6 | version = ">= 5.0.0" 7 | } 8 | } 9 | } 10 | 11 | #tfsec:ignore:aws-ec2-enforce-http-token-imds 12 | resource "aws_instance" "mongo" { 13 | ami = "ami-02868af3c3df4b3aa" 14 | instance_type = var.instance_type 15 | key_name = var.key_name 16 | subnet_id = var.subnet_id 17 | vpc_security_group_ids = [var.sg_id] 18 | root_block_device { 19 | volume_size = 10 20 | encrypted = true 21 | } 22 | 23 | /* 24 | user_data = << EOF 25 | #! /bin/bash 26 | sudo apt-get update 27 | sudo apt-get install -y nginx 28 | sudo systemctl start nginx 29 | sudo systemctl enable nginx 30 | echo "

CloudAcademy 2021

" | sudo tee /var/www/html/index.html 31 | EOF 32 | */ 33 | 34 | #user_data = filebase64("${path.module}/install.sh") 35 | 36 | tags = { 37 | Name = "Mongo" 38 | Owner = "CloudAcademy" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /exercises/exercise5/modules/storage/outputs.tf: -------------------------------------------------------------------------------- 1 | output "private_ip" { 2 | description = "private ip address" 3 | value = aws_instance.mongo.private_ip 4 | } -------------------------------------------------------------------------------- /exercises/exercise5/modules/storage/vars.tf: -------------------------------------------------------------------------------- 1 | variable "instance_type" { 2 | type = string 3 | } 4 | 5 | variable "key_name" { 6 | type = string 7 | } 8 | 9 | variable "subnet_id" { 10 | type = string 11 | } 12 | 13 | variable "sg_id" { 14 | type = string 15 | } 16 | -------------------------------------------------------------------------------- /exercises/exercise5/outputs.tf: -------------------------------------------------------------------------------- 1 | output "alb_dns" { 2 | description = "alb dns" 3 | value = module.application.dns_name 4 | } 5 | 6 | output "bastion_public_ip" { 7 | description = "bastion public ip" 8 | value = module.bastion.public_ip 9 | } 10 | 11 | output "application_private_ips" { 12 | description = "application instance private ips" 13 | value = module.application.private_ips 14 | } 15 | 16 | output "mongodb_private_ip" { 17 | description = "mongodb private ip" 18 | value = module.storage.private_ip 19 | } 20 | 21 | output "web_app_wait_command" { 22 | value = "until curl --max-time 5 http://${module.application.dns_name} >/dev/null 2>&1; do echo preparing...; sleep 5; done; echo; echo -e 'Ready!!'" 23 | description = "Test command - tests readiness of the web app" 24 | } 25 | -------------------------------------------------------------------------------- /exercises/exercise5/terraform.tfvars: -------------------------------------------------------------------------------- 1 | availability_zones = ["us-west-2a", "us-west-2b"] 2 | cidr_block = "10.0.0.0/16" 3 | bastion_instance_type = "t3.micro" 4 | app_instance_type = "t3.micro" 5 | db_instance_type = "t3.micro" 6 | -------------------------------------------------------------------------------- /exercises/exercise5/variables.tf: -------------------------------------------------------------------------------- 1 | variable "key_name" { 2 | type = string 3 | } 4 | 5 | variable "workstation_ip" { 6 | type = string 7 | } 8 | 9 | variable "cidr_block" { 10 | type = string 11 | description = "VPC cidr block. Example: 10.10.0.0/16" 12 | } 13 | 14 | variable "availability_zones" { 15 | type = list(any) 16 | } 17 | 18 | variable "bastion_instance_type" { 19 | type = string 20 | } 21 | 22 | variable "app_instance_type" { 23 | type = string 24 | } 25 | 26 | variable "db_instance_type" { 27 | type = string 28 | } 29 | -------------------------------------------------------------------------------- /exercises/exercise6/README.md: -------------------------------------------------------------------------------- 1 | ## Exercise 6 2 | Launch an EKS cluster and deploy a pre-built cloud native web app. 3 | 4 | ![Stocks App](/doc/stocks.png) 5 | 6 | The following EKS architecture will be provisioned using Terraform: 7 | 8 | ![EKS Cloud Native Application](/doc/eks.png) 9 | 10 | The cloud native web app that gets deployed is based on the following codebase: 11 | 12 | - https://github.com/cloudacademy/stocks-app 13 | - https://github.com/cloudacademy/stocks-api 14 | 15 | The following public AWS **modules** are used to launch the EKS cluster: 16 | 17 | - [VPC](https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/latest) 18 | - [EKS](https://registry.terraform.io/modules/terraform-aws-modules/eks/aws/latest) 19 | 20 | Additionally, the following **providers** are utilised: 21 | 22 | - [hashicorp/helm](https://registry.terraform.io/providers/hashicorp/helm/latest) 23 | - [hashicorp/null](https://registry.terraform.io/providers/hashicorp/null/latest) 24 | 25 | The EKS cluster will be provisioned with 2 worker nodes based on m5.large **spot** instances. This configuration is suitable for the demonstration purposes of this exercise. Production environments are likely more suited to **on-demand always on** instances. 26 | 27 | The cloud native web app deployed is configured within the `./k8s` directory, and is installed automatically using the following null resource configuration: 28 | 29 | ```terraform 30 | resource "null_resource" "deploy_app" { 31 | triggers = { 32 | always_run = "${timestamp()}" 33 | } 34 | 35 | provisioner "local-exec" { 36 | interpreter = ["/bin/bash", "-c"] 37 | working_dir = path.module 38 | command = </dev/null 2>&1; do echo "waiting for nginx ingress controller service to become available..." && sleep 5; done 13 | INGRESS_LB_FQDN=$(kubectl get svc nginx-ingress-controller -n nginx-ingress -o jsonpath="{.status.loadBalancer.ingress[0].hostname}") 14 | echo $INGRESS_LB_FQDN 15 | 16 | until nslookup $INGRESS_LB_FQDN >/dev/null 2>&1; do echo "waiting for DNS propagation..." && sleep 5; done 17 | INGRESS_PUBLIC_IP=$(dig +short $INGRESS_LB_FQDN | head -n1) 18 | echo $INGRESS_PUBLIC_IP 19 | 20 | API_PUBLIC_FQDN=cloudacademy.api.$INGRESS_PUBLIC_IP.nip.io 21 | FRONTEND_PUBLIC_FQDN=cloudacademy.frontend.$INGRESS_PUBLIC_IP.nip.io 22 | 23 | echo $API_PUBLIC_FQDN 24 | echo $FRONTEND_PUBLIC_FQDN 25 | 26 | # =========================== 27 | 28 | echo -e "\nSTEP3: updating K8s manifest files...\n" 29 | 30 | sed \ 31 | -e "s/API_HOST_INGRESS_FQDN/${API_PUBLIC_FQDN}/g" \ 32 | ./k8s/templates/2_api.yaml > ./k8s/manifests/2_api.yaml 33 | 34 | sed \ 35 | -e "s/API_HOST_INGRESS_FQDN/${API_PUBLIC_FQDN}/g" \ 36 | -e "s/FRONTEND_HOST_INGRESS_FQDN/${FRONTEND_PUBLIC_FQDN}/g" \ 37 | ./k8s/templates/3_frontend.yaml > ./k8s/manifests/3_frontend.yaml 38 | 39 | # =========================== 40 | 41 | echo -e "\nSTEP4: deploying application...\n" 42 | 43 | kubectl apply -f ./k8s/manifests/ 44 | 45 | echo -e "\ndeployment finished\n" -------------------------------------------------------------------------------- /exercises/exercise6/k8s/manifests/.gitignore: -------------------------------------------------------------------------------- 1 | 2_api.yaml 2 | 3_frontend.yaml -------------------------------------------------------------------------------- /exercises/exercise6/k8s/manifests/1_db.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: db 5 | namespace: cloudacademy 6 | labels: 7 | role: db 8 | env: demo 9 | spec: 10 | replicas: 1 11 | strategy: 12 | type: RollingUpdate 13 | rollingUpdate: 14 | maxSurge: 1 15 | maxUnavailable: 25% 16 | selector: 17 | matchLabels: 18 | role: db 19 | template: 20 | metadata: 21 | labels: 22 | role: db 23 | spec: 24 | containers: 25 | - name: db 26 | image: cloudacademydevops/stocks-db:v1 27 | imagePullPolicy: Always 28 | env: 29 | - name: MYSQL_ROOT_PASSWORD 30 | value: fo11owth3wh1t3r4bb1t 31 | ports: 32 | - containerPort: 3306 33 | tolerations: 34 | - key: "eks.amazonaws.com/compute-type" 35 | operator: "Equal" 36 | value: "fargate" 37 | effect: "NoSchedule" 38 | --- 39 | apiVersion: v1 40 | kind: Service 41 | metadata: 42 | name: db 43 | namespace: cloudacademy 44 | labels: 45 | role: db 46 | env: demo 47 | spec: 48 | ports: 49 | - protocol: TCP 50 | port: 3306 51 | selector: 52 | role: db 53 | -------------------------------------------------------------------------------- /exercises/exercise6/k8s/templates/2_api.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: api 5 | namespace: cloudacademy 6 | labels: 7 | role: api 8 | env: demo 9 | spec: 10 | replicas: 2 11 | strategy: 12 | type: RollingUpdate 13 | rollingUpdate: 14 | maxSurge: 1 15 | maxUnavailable: 25% 16 | selector: 17 | matchLabels: 18 | role: api 19 | template: 20 | metadata: 21 | labels: 22 | role: api 23 | spec: 24 | containers: 25 | - name: api 26 | image: cloudacademydevops/stocks-api:v1 27 | imagePullPolicy: Always 28 | env: 29 | - name: DB_CONNSTR 30 | value: jdbc:mysql://db:3306/stocks 31 | - name: DB_USER 32 | value: root 33 | - name: DB_PASSWORD 34 | value: fo11owth3wh1t3r4bb1t 35 | ports: 36 | - containerPort: 8080 37 | --- 38 | apiVersion: v1 39 | kind: Service 40 | metadata: 41 | name: api 42 | namespace: cloudacademy 43 | labels: 44 | role: api 45 | env: demo 46 | spec: 47 | ports: 48 | - protocol: TCP 49 | port: 8080 50 | selector: 51 | role: api 52 | --- 53 | apiVersion: networking.k8s.io/v1 54 | kind: Ingress 55 | metadata: 56 | name: api 57 | namespace: cloudacademy 58 | annotations: 59 | nginx.ingress.kubernetes.io/rewrite-target: / 60 | spec: 61 | ingressClassName: nginx 62 | rules: 63 | - host: API_HOST_INGRESS_FQDN 64 | http: 65 | paths: 66 | - path: / 67 | pathType: Prefix 68 | backend: 69 | service: 70 | name: api 71 | port: 72 | number: 8080 73 | 74 | -------------------------------------------------------------------------------- /exercises/exercise6/k8s/templates/3_frontend.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: frontend 5 | namespace: cloudacademy 6 | labels: 7 | role: frontend 8 | env: demo 9 | spec: 10 | replicas: 4 11 | strategy: 12 | type: RollingUpdate 13 | rollingUpdate: 14 | maxSurge: 1 15 | maxUnavailable: 25% 16 | selector: 17 | matchLabels: 18 | role: frontend 19 | template: 20 | metadata: 21 | labels: 22 | role: frontend 23 | spec: 24 | containers: 25 | - name: frontend 26 | image: cloudacademydevops/stocks-app:v1 27 | imagePullPolicy: Always 28 | env: 29 | - name: REACT_APP_APIHOSTPORT 30 | value: API_HOST_INGRESS_FQDN 31 | ports: 32 | - containerPort: 8080 33 | --- 34 | apiVersion: v1 35 | kind: Service 36 | metadata: 37 | name: frontend 38 | namespace: cloudacademy 39 | labels: 40 | role: frontend 41 | env: demo 42 | spec: 43 | ports: 44 | - protocol: TCP 45 | port: 8080 46 | selector: 47 | role: frontend 48 | --- 49 | apiVersion: networking.k8s.io/v1 50 | kind: Ingress 51 | metadata: 52 | name: frontend 53 | namespace: cloudacademy 54 | annotations: 55 | nginx.ingress.kubernetes.io/rewrite-target: / 56 | spec: 57 | ingressClassName: nginx 58 | rules: 59 | - host: FRONTEND_HOST_INGRESS_FQDN 60 | http: 61 | paths: 62 | - path: / 63 | pathType: Prefix 64 | backend: 65 | service: 66 | name: frontend 67 | port: 68 | number: 8080 69 | -------------------------------------------------------------------------------- /exercises/exercise6/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.4.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = ">= 5.0.0" 8 | } 9 | helm = { 10 | source = "hashicorp/helm" 11 | version = ">= 2.10.0" 12 | } 13 | # null = { 14 | # source = "hashicorp/null" 15 | # version = ">= 3.2.0" 16 | # } 17 | } 18 | } 19 | 20 | provider "aws" { 21 | region = "us-west-2" 22 | } 23 | 24 | data "aws_availability_zones" "available" {} 25 | 26 | locals { 27 | name = "cloudacademydevops" 28 | environment = "demo" 29 | k8s_version = "1.27" 30 | 31 | vpc_cidr = "10.0.0.0/16" 32 | azs = slice(data.aws_availability_zones.available.names, 0, 3) 33 | } 34 | 35 | #==================================== 36 | 37 | module "vpc" { 38 | source = "terraform-aws-modules/vpc/aws" 39 | version = ">= 5.0.0" 40 | 41 | name = local.name 42 | cidr = local.vpc_cidr 43 | 44 | azs = local.azs 45 | private_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 4, k)] 46 | public_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 48)] 47 | intra_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 52)] 48 | 49 | enable_nat_gateway = true 50 | single_nat_gateway = true 51 | one_nat_gateway_per_az = false 52 | 53 | manage_default_network_acl = true 54 | manage_default_route_table = true 55 | manage_default_security_group = true 56 | 57 | default_network_acl_tags = { 58 | Name = "${local.name}-default" 59 | } 60 | 61 | default_route_table_tags = { 62 | Name = "${local.name}-default" 63 | } 64 | 65 | default_security_group_tags = { 66 | Name = "${local.name}-default" 67 | } 68 | 69 | public_subnet_tags = { 70 | "kubernetes.io/role/elb" = 1 71 | } 72 | 73 | private_subnet_tags = { 74 | "kubernetes.io/role/internal-elb" = 1 75 | } 76 | 77 | tags = { 78 | Name = "${local.name}-eks" 79 | Environment = local.environment 80 | } 81 | } 82 | 83 | #==================================== 84 | 85 | module "eks" { 86 | source = "terraform-aws-modules/eks/aws" 87 | version = ">= 19.15.0" 88 | 89 | cluster_name = "${local.name}-eks-2025" 90 | cluster_version = local.k8s_version 91 | 92 | cluster_endpoint_public_access = true 93 | 94 | cluster_addons = { 95 | coredns = {} 96 | kube-proxy = {} 97 | vpc-cni = {} 98 | } 99 | 100 | vpc_id = module.vpc.vpc_id 101 | subnet_ids = module.vpc.private_subnets 102 | 103 | eks_managed_node_groups = { 104 | default = { 105 | use_custom_launch_template = false 106 | 107 | instance_types = ["m5.large"] 108 | capacity_type = "SPOT" # useful for demos and dev purposes 109 | # capacity_type = "ON_DEMAND" 110 | 111 | disk_size = 10 112 | 113 | min_size = 2 114 | max_size = 2 115 | desired_size = 2 116 | } 117 | } 118 | 119 | tags = { 120 | Name = "${local.name}-eks" 121 | Environment = local.environment 122 | } 123 | } 124 | 125 | #==================================== 126 | 127 | provider "helm" { 128 | kubernetes { 129 | host = module.eks.cluster_endpoint 130 | cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data) 131 | 132 | exec { 133 | api_version = "client.authentication.k8s.io/v1beta1" 134 | command = "aws" 135 | args = ["eks", "get-token", "--cluster-name", module.eks.cluster_name] 136 | } 137 | } 138 | } 139 | 140 | resource "helm_release" "nginx_ingress" { 141 | name = "nginx-ingress" 142 | 143 | repository = "https://helm.nginx.com/stable" 144 | chart = "nginx-ingress" 145 | namespace = "nginx-ingress" 146 | create_namespace = true 147 | 148 | set { 149 | name = "service.type" 150 | value = "ClusterIP" 151 | } 152 | 153 | set { 154 | name = "controller.service.name" 155 | value = "nginx-ingress-controller" 156 | } 157 | } 158 | 159 | # resource "helm_release" "argo" { 160 | # name = "argo" 161 | 162 | # repository = "https://argoproj.github.io/argo-helm" 163 | # chart = "argo-cd" 164 | # namespace = "argo" 165 | # create_namespace = true 166 | 167 | # # set { 168 | # # name = "service.type" 169 | # # value = "ClusterIP" 170 | # # } 171 | 172 | # # set { 173 | # # name = "controller.service.name" 174 | # # value = "nginx-ingress-controller" 175 | # # } 176 | # } 177 | 178 | resource "terraform_data" "deploy_app" { 179 | triggers_replace = { 180 | always_run = "${timestamp()}" 181 | } 182 | 183 | provisioner "local-exec" { 184 | interpreter = ["/bin/bash", "-c"] 185 | working_dir = path.module 186 | command = < ./k8s/manifests/2_api.yaml 40 | 41 | sed \ 42 | -e "s/API_HOST_INGRESS_FQDN/${API_PUBLIC_FQDN}/g" \ 43 | -e "s/FRONTEND_HOST_INGRESS_FQDN/${FRONTEND_PUBLIC_FQDN}/g" \ 44 | ./k8s/templates/3_frontend.yaml > ./k8s/manifests/3_frontend.yaml 45 | 46 | =========================== 47 | 48 | kubectl apply -f ./k8s/manifests/ 49 | 50 | =========================== 51 | 52 | watch -n5 "kubectl get pods" 53 | 54 | curl -s $API_PUBLIC_FQDN/api/stocks/csv 55 | 56 | curl -i http://$FRONTEND_PUBLIC_FQDN 57 | 58 | echo "http://$FRONTEND_PUBLIC_FQDN" -------------------------------------------------------------------------------- /exercises/exercise7/.gitignore: -------------------------------------------------------------------------------- 1 | archive/*.zip -------------------------------------------------------------------------------- /exercises/exercise7/api_gw.tf: -------------------------------------------------------------------------------- 1 | resource "aws_apigatewayv2_api" "main" { 2 | name = "main" 3 | protocol_type = "HTTP" 4 | } 5 | 6 | resource "aws_apigatewayv2_stage" "dev" { 7 | api_id = aws_apigatewayv2_api.main.id 8 | 9 | name = "dev" 10 | auto_deploy = true 11 | 12 | access_log_settings { 13 | destination_arn = aws_cloudwatch_log_group.main_api_gw.arn 14 | 15 | format = jsonencode({ 16 | requestId = "$context.requestId" 17 | sourceIp = "$context.identity.sourceIp" 18 | requestTime = "$context.requestTime" 19 | protocol = "$context.protocol" 20 | httpMethod = "$context.httpMethod" 21 | resourcePath = "$context.resourcePath" 22 | routeKey = "$context.routeKey" 23 | status = "$context.status" 24 | responseLength = "$context.responseLength" 25 | integrationErrorMessage = "$context.integrationErrorMessage" 26 | } 27 | ) 28 | } 29 | } 30 | 31 | resource "aws_cloudwatch_log_group" "main_api_gw" { 32 | name = "/aws/api-gw/${aws_apigatewayv2_api.main.name}" 33 | 34 | retention_in_days = 30 35 | } 36 | 37 | #==================================== 38 | 39 | resource "aws_apigatewayv2_integration" "bitcoin" { 40 | api_id = aws_apigatewayv2_api.main.id 41 | 42 | integration_type = "AWS_PROXY" 43 | integration_uri = module.lambda_function["bitcoin"].invoke_arn 44 | } 45 | 46 | resource "aws_apigatewayv2_route" "bitcoin" { 47 | api_id = aws_apigatewayv2_api.main.id 48 | route_key = "GET /bitcoin" 49 | 50 | target = "integrations/${aws_apigatewayv2_integration.bitcoin.id}" 51 | } 52 | 53 | resource "aws_lambda_permission" "bitcoin" { 54 | statement_id = "AllowExecutionFromAPIGateway" 55 | action = "lambda:InvokeFunction" 56 | function_name = module.lambda_function["bitcoin"].function_name 57 | principal = "apigateway.amazonaws.com" 58 | 59 | source_arn = "${aws_apigatewayv2_api.main.execution_arn}/*/*" 60 | } 61 | 62 | #==================================== 63 | 64 | resource "aws_apigatewayv2_integration" "hello" { 65 | api_id = aws_apigatewayv2_api.main.id 66 | 67 | integration_type = "AWS_PROXY" 68 | integration_uri = module.lambda_function["hello"].invoke_arn 69 | } 70 | 71 | resource "aws_apigatewayv2_route" "hello" { 72 | api_id = aws_apigatewayv2_api.main.id 73 | route_key = "GET /hello" 74 | 75 | target = "integrations/${aws_apigatewayv2_integration.hello.id}" 76 | } 77 | 78 | resource "aws_lambda_permission" "hello" { 79 | statement_id = "AllowExecutionFromAPIGateway" 80 | action = "lambda:InvokeFunction" 81 | function_name = module.lambda_function["hello"].function_name 82 | principal = "apigateway.amazonaws.com" 83 | 84 | source_arn = "${aws_apigatewayv2_api.main.execution_arn}/*/*" 85 | } 86 | 87 | #==================================== 88 | 89 | resource "aws_apigatewayv2_integration" "pi" { 90 | api_id = aws_apigatewayv2_api.main.id 91 | 92 | integration_type = "AWS_PROXY" 93 | integration_uri = module.lambda_function["pi"].invoke_arn 94 | } 95 | 96 | resource "aws_apigatewayv2_route" "pi" { 97 | api_id = aws_apigatewayv2_api.main.id 98 | route_key = "GET /pi" 99 | 100 | target = "integrations/${aws_apigatewayv2_integration.pi.id}" 101 | } 102 | 103 | resource "aws_lambda_permission" "pi" { 104 | statement_id = "AllowExecutionFromAPIGateway" 105 | action = "lambda:InvokeFunction" 106 | function_name = module.lambda_function["pi"].function_name 107 | principal = "apigateway.amazonaws.com" 108 | 109 | source_arn = "${aws_apigatewayv2_api.main.execution_arn}/*/*" 110 | } 111 | -------------------------------------------------------------------------------- /exercises/exercise7/fns/bitcoin/code/lambda_function.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import json 3 | import urllib3 4 | 5 | http = urllib3.PoolManager() 6 | 7 | def lambda_handler(event, context): 8 | logging.info('retreives current bitcoin exchange rates') 9 | 10 | try: 11 | response = http.request('GET', 'https://api.coindesk.com/v1/bpi/currentprice.json') 12 | 13 | return { 14 | "statusCode": 200, 15 | "isBase64Encoded": False, 16 | "headers": {"Content-Type": "application/json"}, 17 | "body": response.data 18 | } 19 | 20 | except: 21 | pass 22 | 23 | data = "invalid response from coindesk api" 24 | 25 | return { 26 | "statusCode": 503, 27 | "isBase64Encoded": False, 28 | "headers": {"Content-Type": "application/json"}, 29 | "body": data 30 | } -------------------------------------------------------------------------------- /exercises/exercise7/fns/hello/code/lambda_function.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import json 3 | 4 | def lambda_handler(event, context): 5 | logging.info('Python HTTP trigger function processed a request.') 6 | msg = "Terrarform + Azure Function Apps = 👍👍👍👍" 7 | 8 | try: 9 | name = event["queryStringParameters"]['name'] 10 | if not name: 11 | try: 12 | req_body = json.parse(event.body) 13 | except ValueError: 14 | pass 15 | else: 16 | name = req_body.get('name') 17 | 18 | if name: 19 | return { 20 | "statusCode": 200, 21 | "isBase64Encoded": 'false', 22 | "headers": {"Content-Type": "application/json"}, 23 | "body": f"\n{msg}\n{name} was here!!\n\n" 24 | } 25 | except: 26 | pass 27 | 28 | return { 29 | "statusCode": 200, 30 | "isBase64Encoded": 'false', 31 | "headers": {"Content-Type": "application/json"}, 32 | "body": f"\n{msg}\n\n" 33 | } 34 | -------------------------------------------------------------------------------- /exercises/exercise7/fns/pi/code/lambda_function.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | def lambda_handler(event, context): 4 | logging.info('calculates pi to n decimal places...') 5 | 6 | try: 7 | num = event["queryStringParameters"]['num'] 8 | 9 | if num: 10 | digits = [str(n) for n in list(pi_digits(int(num)))] 11 | pi = "%s.%s\n" % (digits.pop(0), "".join(digits)) 12 | return { 13 | "statusCode": 200, 14 | "isBase64Encoded": 'false', 15 | "headers": {"Content-Type": "application/json"}, 16 | "body": f"\n{pi}\n\n" 17 | } 18 | except: 19 | pass 20 | 21 | return { 22 | "statusCode": 200, 23 | "isBase64Encoded": 'false', 24 | "headers": {"Content-Type": "application/json"}, 25 | "body": f"\n{0}\n\n" 26 | } 27 | 28 | def pi_digits(x): 29 | k,a,b,a1,b1 = 2,4,1,12,4 30 | while x > 0: 31 | p,q,k = k * k, 2 * k + 1, k + 1 32 | a,b,a1,b1 = a1, b1, p*a + q*a1, p*b + q*b1 33 | d,d1 = a/b, a1/b1 34 | while d == d1 and x > 0: 35 | yield int(d) 36 | x -= 1 37 | a,a1 = 10*(a % b), 10*(a1 % b1) 38 | d,d1 = a/b, a1/b1 -------------------------------------------------------------------------------- /exercises/exercise7/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.5" 3 | required_providers { 4 | aws = { 5 | source = "hashicorp/aws" 6 | version = "~> 5.0" 7 | } 8 | archive = { 9 | source = "hashicorp/archive" 10 | version = "~> 2.4" 11 | } 12 | } 13 | } 14 | 15 | provider "aws" { 16 | region = "us-west-2" 17 | } 18 | 19 | data "aws_iam_policy_document" "lambda_assume_role_policy" { 20 | statement { 21 | effect = "Allow" 22 | actions = ["sts:AssumeRole"] 23 | 24 | principals { 25 | type = "Service" 26 | identifiers = ["lambda.amazonaws.com"] 27 | } 28 | } 29 | } 30 | 31 | resource "aws_iam_role" "lambda_role" { 32 | name = "lambda-lambda-role" 33 | assume_role_policy = data.aws_iam_policy_document.lambda_assume_role_policy.json 34 | } 35 | 36 | module "lambda_function" { 37 | source = "./modules/lambda_function" 38 | for_each = { for index, fn in var.lambda_functions : fn.name => fn } 39 | 40 | name = each.value.name 41 | lambda_role_arn = aws_iam_role.lambda_role.arn 42 | source_file = "${path.root}/${each.value.source_file}" 43 | zip_file_name = each.value.zip_file_name 44 | timeout = each.value.timeout 45 | runtime = each.value.runtime 46 | } 47 | -------------------------------------------------------------------------------- /exercises/exercise7/modules/lambda_function/main.tf: -------------------------------------------------------------------------------- 1 | data "archive_file" "lambda" { 2 | type = "zip" 3 | source_file = var.source_file 4 | output_path = var.zip_file_name 5 | } 6 | 7 | resource "aws_lambda_function" "lambda" { 8 | function_name = var.name 9 | filename = var.zip_file_name 10 | source_code_hash = data.archive_file.lambda.output_base64sha256 11 | role = var.lambda_role_arn 12 | runtime = var.runtime 13 | handler = "lambda_function.lambda_handler" 14 | timeout = var.timeout 15 | } 16 | 17 | resource "aws_lambda_function_url" "lambda" { 18 | function_name = aws_lambda_function.lambda.function_name 19 | authorization_type = "NONE" 20 | 21 | cors { 22 | allow_credentials = true 23 | allow_origins = ["*"] 24 | allow_methods = ["*"] 25 | allow_headers = ["date", "keep-alive"] 26 | expose_headers = ["keep-alive", "date"] 27 | max_age = 86400 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /exercises/exercise7/modules/lambda_function/outputs.tf: -------------------------------------------------------------------------------- 1 | output "function_url" { 2 | value = aws_lambda_function_url.lambda.function_url 3 | } 4 | 5 | output "invoke_arn" { 6 | value = aws_lambda_function.lambda.invoke_arn 7 | } 8 | 9 | output "function_name" { 10 | value = aws_lambda_function.lambda.function_name 11 | } 12 | -------------------------------------------------------------------------------- /exercises/exercise7/modules/lambda_function/vars.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | type = string 3 | } 4 | 5 | variable "lambda_role_arn" { 6 | type = string 7 | } 8 | 9 | variable "source_file" { 10 | type = string 11 | } 12 | 13 | variable "zip_file_name" { 14 | type = string 15 | } 16 | 17 | variable "timeout" { 18 | type = string 19 | } 20 | 21 | variable "runtime" { 22 | type = string 23 | } 24 | -------------------------------------------------------------------------------- /exercises/exercise7/outputs.tf: -------------------------------------------------------------------------------- 1 | output "bitcoin_function_url" { 2 | value = module.lambda_function["bitcoin"].function_url 3 | } 4 | 5 | output "bitcoin_api_gateway_url" { 6 | value = "${aws_apigatewayv2_stage.dev.invoke_url}/bitcoin" 7 | } 8 | 9 | output "hello_function_url" { 10 | value = module.lambda_function["hello"].function_url 11 | } 12 | 13 | output "hello_api_gateway_url" { 14 | value = "${aws_apigatewayv2_stage.dev.invoke_url}/hello" 15 | } 16 | 17 | output "pi_function_url" { 18 | value = module.lambda_function["pi"].function_url 19 | } 20 | 21 | output "pi_api_gateway_url" { 22 | value = "${aws_apigatewayv2_stage.dev.invoke_url}/pi" 23 | } 24 | -------------------------------------------------------------------------------- /exercises/exercise7/vars.tf: -------------------------------------------------------------------------------- 1 | variable "lambda_functions" { 2 | type = list(object({ 3 | name = string 4 | source_file = string 5 | zip_file_name = string 6 | timeout = number 7 | runtime = string 8 | })) 9 | description = "list of lambda functions to create" 10 | default = [ 11 | { 12 | name = "bitcoin" 13 | source_file = "./fns/bitcoin/code/lambda_function.py" 14 | zip_file_name = "./archive/fn.bitcoin.zip" 15 | timeout = 60 16 | runtime = "python3.9" 17 | }, 18 | { 19 | name = "hello" 20 | source_file = "./fns/hello/code/lambda_function.py" 21 | zip_file_name = "./archive/fn.hello.zip" 22 | timeout = 60 23 | runtime = "python3.9" 24 | }, 25 | { 26 | name = "pi" 27 | source_file = "./fns/pi/code/lambda_function.py" 28 | zip_file_name = "./archive/fn.pi.zip" 29 | timeout = 60 30 | runtime = "python3.9" 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /exercises/exercise8/.gitignore: -------------------------------------------------------------------------------- 1 | *.pem -------------------------------------------------------------------------------- /exercises/exercise8/domain-join.ps1: -------------------------------------------------------------------------------- 1 | 2 | <# Install Dependencies #>; 3 | Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force; 4 | Install-Module -Name AWS.Tools.Installer -Scope AllUsers -Force; 5 | <# Define Variables #>; 6 | $ad_secret_id = "${ad_secret_id}"; 7 | $ad_domain = "${ad_domain}"; 8 | <# Read secret from the Secret Manager #>; 9 | $secret_manager = Get-SECSecretValue -SecretId $ad_secret_id; 10 | <# Convert the Secret JSON into an object #>; 11 | $ad_secret = $secret_manager.SecretString | ConvertFrom-Json; 12 | <# Set Credentials #>; 13 | $username = $ad_secret.Username + "@" + $ad_domain; 14 | $password = $ad_secret.Password | ConvertTo-SecureString -AsPlainText -Force; 15 | $credential = New-Object System.Management.Automation.PSCredential($username, $password); 16 | <# Join AD Domain #>; 17 | Add-Computer -DomainName $ad_domain -Credential $credential -Passthru -Verbose -Force -Restart; 18 | 19 | -------------------------------------------------------------------------------- /exercises/exercise8/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.4" 3 | required_providers { 4 | aws = { 5 | source = "hashicorp/aws" 6 | version = ">= 5.0.0" 7 | } 8 | random = { 9 | source = "hashicorp/random" 10 | version = ">= 3.5.0" 11 | } 12 | template = { 13 | source = "hashicorp/template" 14 | version = ">= 2.2.0" 15 | } 16 | tls = { 17 | source = "hashicorp/tls" 18 | version = ">= 4.0.0" 19 | } 20 | } 21 | } 22 | 23 | provider "aws" { 24 | region = "us-west-2" 25 | } 26 | 27 | data "aws_availability_zones" "available" {} 28 | 29 | //======================================== 30 | 31 | resource "random_password" "ad_admin_password" { 32 | length = 16 33 | special = true 34 | override_special = "_%@" 35 | } 36 | 37 | resource "random_password" "secret_name_suffix" { 38 | length = 4 39 | special = false 40 | numeric = false 41 | } 42 | 43 | locals { 44 | environment = "prod" 45 | secret_id = "ad-domain-admin-${random_password.secret_name_suffix.result}" 46 | domain_name = "demo.cloudacademydevops.internal" 47 | domain_netbios_name = "CADEVOPS" 48 | domain_computer_ou = "ou=demo,dc=cloudacademydevops,dc=internal" 49 | 50 | vpc_cidr = "10.0.0.0/16" 51 | azs = slice(data.aws_availability_zones.available.names, 0, 3) 52 | } 53 | 54 | //======================================== 55 | 56 | resource "aws_secretsmanager_secret" "ad_domain" { 57 | name = local.secret_id 58 | } 59 | 60 | resource "aws_secretsmanager_secret_version" "ad_creds" { 61 | secret_id = aws_secretsmanager_secret.ad_domain.id 62 | secret_string = <