commit 98cb1575939ca8e5e39e12bb0215c99b92e06755 Author: Paul Ernst Werler Date: Sat Jan 20 22:28:11 2024 +0000 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5dfe310 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.terraform* +terraform.tfstate* diff --git a/README.md b/README.md new file mode 100644 index 0000000..845eb4c --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +### Architecture Ref. Card 03 + +Für das Projekt kann ein eigenes Schlüsselpaar verwendet werden. Der Name des öffentlichen Schlüssels ist per default auf "ansible.pub" gesetzt - kann aber in der variables.tf angepasst werden. Der private Schlüssel könnte auch irgendwo anders liegen - in der variables.tf anpassen! + +Terraform installiert eine RDS-Instanz (mariadb) und eine EC2-Instanz. Auf der EC2-Instanz wird zusätzlich ein mariadb-Server via Ansible installiert, da die Applikation die Datenbank-URL hart kodiert hat... + +Ansible wird direkt durch Terraform gestartet. Man könnte das auch separat machen und die Ausgabe von "tofu output" oder "terraform output" direkt weiter verwursten. + +1. Erzeugen eines Schlüsselpaares +``` +ssh-keygen -f keys/ansible -P '' +``` +2. tofu oder terraform init +3. tofu oder terraform plan -out="name des jobs z.B. jokesdb" +4. tofu oder terraform apply "name des jobs z.B. jokesdb" +5. have fun + +Was man noch so alles machen könnte: +1. Portumleitung ingress 80 -> localhost 8080 oder besser +2. Reverse-Proxy (z.B. nginx) und stattdessen ingress 80 -> ingress 443 und ingress 443 nach localhost 8080. +3. Zertifikat mit LetsEncrypt drauf packen +4. Applikation direkt auf dem Server kompilieren +5. Applikation patchen, dass sie auch DDB_URI unterstützt... diff --git a/ansible/all.yml b/ansible/all.yml new file mode 100644 index 0000000..04662ac --- /dev/null +++ b/ansible/all.yml @@ -0,0 +1,8 @@ +--- +- hosts: + - all + roles: + - db + - web + become: true + gather_facts: false diff --git a/ansible/database.yml b/ansible/database.yml new file mode 100644 index 0000000..491f923 --- /dev/null +++ b/ansible/database.yml @@ -0,0 +1,9 @@ +--- +- name: install and configure database + hosts: + - all + roles: + - db + become: true + gather_facts: false + diff --git a/ansible/roles/db/files/data.sql b/ansible/roles/db/files/data.sql new file mode 100644 index 0000000..437fc39 --- /dev/null +++ b/ansible/roles/db/files/data.sql @@ -0,0 +1,12 @@ + + +INSERT INTO `section` (`id`, `name`) VALUES +(1, 'Flachwitze'), +(2, 'Schwarzer Humor'); + +INSERT INTO `joke` (`id`, `section_idfs`, `text`, `rating`, `creation_date`) VALUES +(1, 1, 'Kunde: \"Ich möchte Ihren Chef sprechen!\"\r\nSekretärin: \"Geht leider nicht, er ist nicht da!\"\r\nKunde: \"Ich hab ihn doch durchs Fenster gesehen!\"\r\nSekretärin: \"Er Sie auch!\"', 5, '2014-01-08 21:39:40'), +(2, 1, 'Der Verwaltungsrat zum CEO:\r\n\"Na, wie macht sich denn der neue Buchhalter?\"\r\nCEO: \"Toll, dieser Mann!\"\r\nVerwaltungsrat: \"Was kann er denn so besonderes?\"\r\nCEO: \"Er ist gelernter Friseur, er kann frisieren!\"', 3, '2014-01-08 21:42:41'), +(3, 1, 'Chef: \"Müller, Sie sind das beste Pferd in meinem Stall!\"\r\nMüller: \"Wirklich, Chef?\"\r\nChef: \"Ja, Sie machen den meisten Mist!\"', 5, '2014-01-08 21:43:20'), +(6, 2, 'Was steht auf dem Grabstein eines Mathematikers?\r\n\"Damit hat er nicht gerechnet.\"', 3, '2021-04-06 12:47:17'); + diff --git a/ansible/roles/db/files/schema.sql b/ansible/roles/db/files/schema.sql new file mode 100644 index 0000000..ab8686a --- /dev/null +++ b/ansible/roles/db/files/schema.sql @@ -0,0 +1,26 @@ +DROP TABLE IF EXISTS section; +CREATE TABLE `section` ( + `id` int(11) NOT NULL, + `name` varchar(255) COLLATE utf8_bin NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin; + +DROP TABLE IF EXISTS joke; +CREATE TABLE `joke` ( + `id` int(11) NOT NULL, + `section_idfs` int(11) NOT NULL, + `text` text COLLATE utf8_bin NOT NULL, + `rating` int(11) NOT NULL, + `creation_date` datetime NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin; + + + + +-- +-- Indizes für die Tabelle `joke` +-- +ALTER TABLE `joke` ADD PRIMARY KEY (`id`); +ALTER TABLE `section` ADD PRIMARY KEY (`id`); +ALTER TABLE `joke` MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=7; +ALTER TABLE `section` MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3; + diff --git a/ansible/roles/db/handlers/main.yml b/ansible/roles/db/handlers/main.yml new file mode 100644 index 0000000..ba971e3 --- /dev/null +++ b/ansible/roles/db/handlers/main.yml @@ -0,0 +1,3 @@ +--- +- name: restart mariadb + service: name=mysql state=restarted diff --git a/ansible/roles/db/tasks/main.yml b/ansible/roles/db/tasks/main.yml new file mode 100644 index 0000000..dab9a27 --- /dev/null +++ b/ansible/roles/db/tasks/main.yml @@ -0,0 +1,52 @@ +--- +- name: update + apt: update_cache=yes + ignore_errors: yes + +- name: install mariadb server because jokesdb has localhost hard coded + apt: name=mariadb-server state=latest + notify: restart mariadb + +- name: install mariadb client + apt: name=mariadb-client state=latest + +- name: install python3-pymysql + apt: name=python3-pymysql state=latest + +- name: create a new database with name "{{ db_name }}" + community.mysql.mysql_db: + name: "{{ db_name }}" + state: present + login_unix_socket: /run/mysqld/mysqld.sock + +- name: create database user with all database privileges + community.mysql.mysql_user: + name: "{{ db_username }}" + password: "{{ db_password }}" + priv: "{{ db_name }}.*:ALL" + state: present + login_unix_socket: /run/mysqld/mysqld.sock + +- name: copy database schema file + copy: + src: files/schema.sql + dest: /tmp + +- name: copy database data file + copy: + src: files/data.sql + dest: /tmp + +- name: import db schema + community.mysql.mysql_db: + state: import + name: "{{ db_name }}" + target: /tmp/schema.sql + login_unix_socket: /run/mysqld/mysqld.sock + +- name: import db data + community.mysql.mysql_db: + state: import + name: "{{ db_name }}" + target: /tmp/data.sql + login_unix_socket: /run/mysqld/mysqld.sock diff --git a/ansible/roles/web/files/architecture-refcard-03-0.0.1-SNAPSHOT.jar b/ansible/roles/web/files/architecture-refcard-03-0.0.1-SNAPSHOT.jar new file mode 100644 index 0000000..dc317ec Binary files /dev/null and b/ansible/roles/web/files/architecture-refcard-03-0.0.1-SNAPSHOT.jar differ diff --git a/ansible/roles/web/files/data.sql b/ansible/roles/web/files/data.sql new file mode 100644 index 0000000..437fc39 --- /dev/null +++ b/ansible/roles/web/files/data.sql @@ -0,0 +1,12 @@ + + +INSERT INTO `section` (`id`, `name`) VALUES +(1, 'Flachwitze'), +(2, 'Schwarzer Humor'); + +INSERT INTO `joke` (`id`, `section_idfs`, `text`, `rating`, `creation_date`) VALUES +(1, 1, 'Kunde: \"Ich möchte Ihren Chef sprechen!\"\r\nSekretärin: \"Geht leider nicht, er ist nicht da!\"\r\nKunde: \"Ich hab ihn doch durchs Fenster gesehen!\"\r\nSekretärin: \"Er Sie auch!\"', 5, '2014-01-08 21:39:40'), +(2, 1, 'Der Verwaltungsrat zum CEO:\r\n\"Na, wie macht sich denn der neue Buchhalter?\"\r\nCEO: \"Toll, dieser Mann!\"\r\nVerwaltungsrat: \"Was kann er denn so besonderes?\"\r\nCEO: \"Er ist gelernter Friseur, er kann frisieren!\"', 3, '2014-01-08 21:42:41'), +(3, 1, 'Chef: \"Müller, Sie sind das beste Pferd in meinem Stall!\"\r\nMüller: \"Wirklich, Chef?\"\r\nChef: \"Ja, Sie machen den meisten Mist!\"', 5, '2014-01-08 21:43:20'), +(6, 2, 'Was steht auf dem Grabstein eines Mathematikers?\r\n\"Damit hat er nicht gerechnet.\"', 3, '2021-04-06 12:47:17'); + diff --git a/ansible/roles/web/files/schema.sql b/ansible/roles/web/files/schema.sql new file mode 100644 index 0000000..ab8686a --- /dev/null +++ b/ansible/roles/web/files/schema.sql @@ -0,0 +1,26 @@ +DROP TABLE IF EXISTS section; +CREATE TABLE `section` ( + `id` int(11) NOT NULL, + `name` varchar(255) COLLATE utf8_bin NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin; + +DROP TABLE IF EXISTS joke; +CREATE TABLE `joke` ( + `id` int(11) NOT NULL, + `section_idfs` int(11) NOT NULL, + `text` text COLLATE utf8_bin NOT NULL, + `rating` int(11) NOT NULL, + `creation_date` datetime NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin; + + + + +-- +-- Indizes für die Tabelle `joke` +-- +ALTER TABLE `joke` ADD PRIMARY KEY (`id`); +ALTER TABLE `section` ADD PRIMARY KEY (`id`); +ALTER TABLE `joke` MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=7; +ALTER TABLE `section` MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3; + diff --git a/ansible/roles/web/handlers/main.yml b/ansible/roles/web/handlers/main.yml new file mode 100644 index 0000000..ba971e3 --- /dev/null +++ b/ansible/roles/web/handlers/main.yml @@ -0,0 +1,3 @@ +--- +- name: restart mariadb + service: name=mysql state=restarted diff --git a/ansible/roles/web/tasks/main.yml b/ansible/roles/web/tasks/main.yml new file mode 100644 index 0000000..be95cd6 --- /dev/null +++ b/ansible/roles/web/tasks/main.yml @@ -0,0 +1,72 @@ +--- +- name: install jre + apt: name=openjdk-21-jre-headless state=latest + +# the next steps we can skip here because we run the db-role first +# thanks to the java app wich has the db uri hard coded +# +#- name: install mariadb-client +# apt: name=mariadb-client state=latest +# +#- name: install PyMySQL +# apt: name=python3-pymysql state=latest +# +#- name: copy database schema file +# copy: +# src: files/schema.sql +# dest: /tmp +# +#- name: copy database data file +# copy: +# src: files/data.sql +# dest: /tmp + +- name: add user for jokesdb + ansible.builtin.user: + name: jokesdb + comment: user to run application jokesdb + create_home: True + home: /var/jokesdb + +- name: that destination path exists + file: + path: /var/jokesdb/bin + state: directory + +- name: copy architecture-refcard-03-0.0.1-SNAPSHOT.jar + copy: + src: files/architecture-refcard-03-0.0.1-SNAPSHOT.jar + dest: /var/jokesdb/bin + +# we run this only because we want to show that it works + +- name: import db schema + community.mysql.mysql_db: + state: import + name: jokedb + login_host: "{{ db_address }}" + login_user: "{{ db_username }}" + login_password: "{{ db_password }}" + target: /tmp/schema.sql + +- name: import db data + community.mysql.mysql_db: + state: import + name: "{{ db_name }}" + login_host: "{{ db_address }}" + login_user: "{{ db_username }}" + login_password: "{{ db_password }}" + target: /tmp/data.sql + +- name: create default file for application + ansible.builtin.template: + src: templates/jokesdb.j2 + dest: /etc/default/jokesdb + +- name: create service file to start the application + ansible.builtin.template: + src: templates/jokesdb_service.j2 + dest: /etc/systemd/system/jokesdb.service + +- name: start jokesdb + systemd: state=started name=jokesdb daemon_reload=yes diff --git a/ansible/roles/web/templates/jokesdb.j2 b/ansible/roles/web/templates/jokesdb.j2 new file mode 100644 index 0000000..7c73fe7 --- /dev/null +++ b/ansible/roles/web/templates/jokesdb.j2 @@ -0,0 +1,5 @@ +DDB_USERNAME="{{ db_username }}" +DDB_PASSWORD="{{ db_password }}" +DDB_URI="mysql://{{ db_address }}:{{ db_port }}/{{ db_name }}" +JAVA_HOME="/usr/lib/jvm/java-21-openjdk-amd64" +JAR_FILE="/var/jokesdb/bin/architecture-refcard-03-0.0.1-SNAPSHOT.jar" diff --git a/ansible/roles/web/templates/jokesdb_service.j2 b/ansible/roles/web/templates/jokesdb_service.j2 new file mode 100644 index 0000000..6f38592 --- /dev/null +++ b/ansible/roles/web/templates/jokesdb_service.j2 @@ -0,0 +1,19 @@ +[Unit] +Description=JokesDB +After=syslog.target network.target + +[Service] +EnvironmentFile=-/etc/default/jokesdb +SuccessExitStatus=143 + +User=jokesdb +Group=jokesdb + +Type=simple + +WorkingDirectory=/var/jokesdb +ExecStart=/usr/bin/env ${JAVA_HOME}/bin/java -DDB_USERNAME=${DDB_USERNAME} -DDB_PASSWORD=${DDB_PASSWORD} -jar ${JAR_FILE} +ExecStop=/bin/kill -15 $MAINPID + +[Install] +WantedBy=multi-user.target diff --git a/ansible/web.yml b/ansible/web.yml new file mode 100644 index 0000000..30d04a2 --- /dev/null +++ b/ansible/web.yml @@ -0,0 +1,8 @@ +--- +- name: install and configure database server and start application + hosts: + - all + roles: + - web + become: true + gather_facts: false diff --git a/instances.tf b/instances.tf new file mode 100644 index 0000000..1427055 --- /dev/null +++ b/instances.tf @@ -0,0 +1,45 @@ +resource "aws_db_instance" "mariadb" { + allocated_storage = 20 + storage_type = "gp2" + engine = "mariadb" + engine_version = "10.6.14" + instance_class = "db.t3.micro" + db_name = var.db_name + username = var.db_username + password = var.db_password + port = var.db_port + skip_final_snapshot = true + vpc_security_group_ids = [aws_security_group.rds.id] + db_subnet_group_name = aws_db_subnet_group.private.name +} + +resource "aws_instance" "ec2_instance" { + instance_type = "t3.micro" + ami = data.aws_ami.ubuntu_22_04.id + subnet_id = aws_subnet.public.id + vpc_security_group_ids = [aws_security_group.ec2.id] + key_name = aws_key_pair.ansible.key_name + lifecycle { + ignore_changes = [ami] + } + provisioner "local-exec" { + command = "ansible-playbook -u ubuntu --private-key='${var.private_key}' -i '${aws_instance.ec2_instance.public_ip},' --extra-vars 'web_public_ip=${aws_instance.ec2_instance.public_ip} db_address=${aws_db_instance.mariadb.address} db_name=${var.db_name} db_username=${var.db_username} db_password=${var.db_password} db_port=${var.db_port}' ansible/all.yml" + interpreter = ["/bin/bash", "-c"] + environment = { + ANSIBLE_HOST_KEY_CHECKING = "False" + } + } +} + +data "aws_ami" "ubuntu_22_04" { + most_recent = true + filter { + name = "name" + values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"] + } + filter { + name = "virtualization-type" + values = ["hvm"] + } + owners = ["099720109477"] +} diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..3b37828 --- /dev/null +++ b/main.tf @@ -0,0 +1,26 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} + +data "aws_availability_zones" "available" { + state = "available" +} + +provider "aws" { + region = var.region +} + +resource "aws_vpc" "main" { + cidr_block = var.main_cidr + enable_dns_hostnames = true +} + +resource "aws_key_pair" "ansible" { + key_name = "ansible" + public_key = file(var.public_key) +} diff --git a/networking.tf b/networking.tf new file mode 100644 index 0000000..5d2d571 --- /dev/null +++ b/networking.tf @@ -0,0 +1,95 @@ +resource "aws_internet_gateway" "main" { + vpc_id = aws_vpc.main.id +} + +resource "aws_subnet" "public" { + vpc_id = aws_vpc.main.id + cidr_block = var.public_cidr + map_public_ip_on_launch = true +} + +resource "aws_subnet" "private" { + count = 2 + vpc_id = aws_vpc.main.id + cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, 10 + count.index) + availability_zone = element(data.aws_availability_zones.available.names, count.index) + tags = { + Name = "private_${data.aws_availability_zones.available.names[count.index]}" + } +} + +resource "aws_route_table" "public" { + vpc_id = aws_vpc.main.id + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.main.id + } +} + +resource "aws_route_table_association" "public" { + route_table_id = aws_route_table.public.id + subnet_id = aws_subnet.public.id +} + +resource "aws_route_table" "private" { + vpc_id = aws_vpc.main.id +} + +resource "aws_route_table_association" "private" { + count = length(aws_subnet.private) + route_table_id = aws_route_table.private.id + subnet_id = aws_subnet.private[count.index].id +} + +resource "aws_security_group" "ec2" { + name = "ec2-allow-ssh-http" + description = "Security group for the EC2 instance" + vpc_id = aws_vpc.main.id + + ingress { + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + from_port = 80 + to_port = 8080 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_security_group" "rds" { + name = "rds-allow-sql" + description = "Security group for the RDS instance" + vpc_id = aws_vpc.main.id + + ingress { + from_port = 3306 + to_port = 3306 + protocol = "tcp" + security_groups = [aws_security_group.ec2.id] + } + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_db_subnet_group" "private" { + name = "db_subnet_group" + subnet_ids = [for subnet in aws_subnet.private : subnet.id] +} diff --git a/output.tf b/output.tf new file mode 100644 index 0000000..15e5f6e --- /dev/null +++ b/output.tf @@ -0,0 +1,27 @@ +# Output data for input in ansible if ansible will be started manually like with: +# "--extra-vars=$(terraform output --json | jq 'with_entries(.value |= .value)')" + +output "web_public_ip" { + description = "The public IP address of the web server" + value = aws_instance.ec2_instance.public_ip +} +output "db_address" { + description = "The endpoint of the database" + value = aws_db_instance.mariadb.address +} +output "db_name" { + description = "The name of the database" + value = var.db_name +} +output "db_username" { + description = "The user for the database" + value = var.db_username +} +output "db_password" { + description = "The password for the database" + value = var.db_password +} +output "db_port" { + description = "The port of the database" + value = var.db_port +} diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..b14e051 --- /dev/null +++ b/variables.tf @@ -0,0 +1,40 @@ +variable "public_cidr" { + description = "Cidr block main" + default = "10.0.0.0/24" +} +variable "main_cidr" { + description = "Cidr block main" + default = "10.0.0.0/16" +} +variable "region" { + description = "Region for deployment" + default = "us-west-2" +} +variable "db_name" { + description = "The name of the database" + default = "jokedb" +} +variable "db_username" { + description = "The user for the database" + default = "jokedbuser" +} +variable "db_password" { + description = "The password for the database" + default = "a4d2410bcc42ebc32af364e15b4bcd5e" +} +variable "db_port" { + description = "The port of the database" + default = "3306" +} +variable "ssh_key" { + description = "Public private key to access the ec2 instance" + default = "./keys/ansible" +} +variable "private_key" { + description = "Private SSH key to access the ec2 instance" + default = "./keys/ansible" +} +variable "public_key" { + description = "Public SSH key to access the ec2 instance" + default = "./keys/ansible.pub" +}