diff --git a/.markdownlint.rb b/.markdownlint.rb new file mode 100644 index 0000000..717b9f4 --- /dev/null +++ b/.markdownlint.rb @@ -0,0 +1,2 @@ +all +exclude_rule 'MD007' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b96b009..b2faee5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,6 +29,7 @@ repos: rev: v0.13.0 hooks: - id: markdownlint + args: [-s, .markdownlint.rb] - repo: https://github.com/maxbrunet/pre-commit-renovate rev: 37.2.0 hooks: @@ -47,3 +48,7 @@ repos: rev: 0.2.2 hooks: - id: checkmake + - repo: https://github.com/ansible-community/ansible-lint.git + rev: v6.20.2 + hooks: + - id: ansible-lint diff --git a/live/ci/artifactory/terragrunt.hcl b/live/ci/artifactory/terragrunt.hcl new file mode 100644 index 0000000..1c127f1 --- /dev/null +++ b/live/ci/artifactory/terragrunt.hcl @@ -0,0 +1,15 @@ +terraform { + source = "../../../modules//artifactory" +} + +include "root" { + path = find_in_parent_folders() +} + +locals { + environment_vars = read_terragrunt_config(find_in_parent_folders("env.hcl")) +} + +inputs = merge( + local.environment_vars.locals +) diff --git a/live/dev/artifactory/terragrunt.hcl b/live/dev/artifactory/terragrunt.hcl new file mode 100644 index 0000000..1c127f1 --- /dev/null +++ b/live/dev/artifactory/terragrunt.hcl @@ -0,0 +1,15 @@ +terraform { + source = "../../../modules//artifactory" +} + +include "root" { + path = find_in_parent_folders() +} + +locals { + environment_vars = read_terragrunt_config(find_in_parent_folders("env.hcl")) +} + +inputs = merge( + local.environment_vars.locals +) diff --git a/live/prod/artifactory/terragrunt.hcl b/live/prod/artifactory/terragrunt.hcl new file mode 100644 index 0000000..1c127f1 --- /dev/null +++ b/live/prod/artifactory/terragrunt.hcl @@ -0,0 +1,15 @@ +terraform { + source = "../../../modules//artifactory" +} + +include "root" { + path = find_in_parent_folders() +} + +locals { + environment_vars = read_terragrunt_config(find_in_parent_folders("env.hcl")) +} + +inputs = merge( + local.environment_vars.locals +) diff --git a/modules/artifactory/README.md b/modules/artifactory/README.md new file mode 100644 index 0000000..df16502 --- /dev/null +++ b/modules/artifactory/README.md @@ -0,0 +1,86 @@ +# artifactory + +IaC Spinning Artifactory instance on AWS + +## local DEV environment + +### System Requirements + +* AWS [credentials settings][1], profile __default__ +* Define a secret in AWS Secrets Manager: + * Secret name: artifactory + * key __artifactory_license_1__ + * key __username__ (value must be lowercase) + * key __password__ (value must be lowercase) +* `make` +* `podman` or `docker` + +### pre-commit + +To run pre-commit locally, follow the instructions: + +```shell +pip install --user pre-commit +pre-commit install +``` + +### DEV Environment init + +```shell +make init +``` + +### DEV Environment reconfigure + +```shell +make reconfigure +``` + +### DEV Environment Up + +```shell +make up +``` + +### DEV Environment Down + +```shell +make down +``` + +## STAGE environment + +* Setup AWS profile named __stage__ +* Define a secret in AWS Secrets Manager: + * Secret name: artifactory + * key __artifactory_license_2__ + * key __username__ (value must be lowercase) + * key __password__ (value must be lowercase) +* `make` +* `podman` or `docker` + +### STAGE Environment init + +```shell +make init +``` + +### STAGE Environment reconfigure + +```shell +make reconfigure +``` + +### STAGE Environment Up + +```shell +make ENV=stage up +``` + +### STAGE Environment Down + +```shell +make ENV=stage down +``` + +[1]: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html diff --git a/modules/artifactory/main.tf b/modules/artifactory/main.tf new file mode 100644 index 0000000..fe1e7da --- /dev/null +++ b/modules/artifactory/main.tf @@ -0,0 +1,143 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "5.19.0" + } + } +} + +provider "aws" { + region = var.aws_region + profile = var.aws_profile + default_tags { + tags = merge(var.tags, { User = var.user }) + } +} + +data "aws_ami" "centos_stream_8" { + most_recent = true + owners = ["125523088429"] + + filter { + name = "name" + values = ["CentOS Stream 8 *"] + } + + filter { + name = "architecture" + values = ["x86_64"] + } +} + +resource "aws_security_group" "security_group" { + name = var.artifactory_security_group_name + description = "Artifactory inbound and outbound traffic" + + # SSH access from anywhere + ingress { + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + # HTTP access from anywhere + ingress { + from_port = 8082 + to_port = 8082 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + # HTTPS access from anywhere + ingress { + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + # outbound internet access + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } +} + +data "aws_iam_policy_document" "assume_policy" { + statement { + sid = "1" + + principals { + type = "Service" + identifiers = ["ec2.amazonaws.com"] + } + + actions = [ + "sts:AssumeRole" + ] + } +} + +data "aws_iam_policy_document" "secretmanager_iam_policy_document" { + statement { + sid = "1" + + actions = [ + "secretsmanager:GetResourcePolicy", + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret", + "secretsmanager:ListSecretVersionIds", + "secretsmanager:ListSecrets" + ] + + resources = [ + "*", + ] + } +} + +resource "aws_iam_role" "iam_role" { + name = var.artifactory_iam_role_name + assume_role_policy = data.aws_iam_policy_document.assume_policy.json + inline_policy { + name = var.secretsmanager_policy + policy = data.aws_iam_policy_document.secretmanager_iam_policy_document.json + } +} + +resource "aws_iam_instance_profile" "iam_instance_profile" { + name = var.artifactory_iam_instance_profile + role = aws_iam_role.iam_role.name +} + +resource "tls_private_key" "tls_private_key" { + algorithm = "RSA" + rsa_bits = 4096 +} + +resource "aws_key_pair" "key_pair" { + key_name = var.ssh_key_name + public_key = tls_private_key.tls_private_key.public_key_openssh +} + +resource "local_file" "private_key" { + content = tls_private_key.tls_private_key.private_key_openssh + filename = var.ssh_private_file_name + file_permission = 0400 +} + +resource "aws_instance" "artifactory_instance" { + ami = data.aws_ami.centos_stream_8.id + instance_type = var.artifactory_instance_type + vpc_security_group_ids = [aws_security_group.security_group.id] + iam_instance_profile = aws_iam_instance_profile.iam_instance_profile.name + key_name = aws_key_pair.key_pair.key_name + tags = merge(var.tags, { Name = var.artifactory_server_name }) + root_block_device { + volume_size = 100 + } +} diff --git a/modules/artifactory/provision/ansible.cfg b/modules/artifactory/provision/ansible.cfg new file mode 100644 index 0000000..c26bd04 --- /dev/null +++ b/modules/artifactory/provision/ansible.cfg @@ -0,0 +1,9 @@ +[defaults] +host_key_checking=False +private_key_file=/workspace/tf-artifactory-ssh-key.pem +ask_pass=false +inventory=aws_ec2.yml +remote_user=centos + +[inventory] +enable_plugins = aws_ec2 diff --git a/modules/artifactory/provision/aws_ec2.yml b/modules/artifactory/provision/aws_ec2.yml new file mode 100644 index 0000000..3085c63 --- /dev/null +++ b/modules/artifactory/provision/aws_ec2.yml @@ -0,0 +1,10 @@ +plugin: aws_ec2 +aws_profile: "{{ lookup('env', 'AWS_PROFILE') | default('default', true) }}" +regions: + - eu-central-1 + - eu-west-2 +filters: + instance-state-name: running + tag:Project: platform-engineering +include_filters: + - tag:User: "{{ lookup('env', 'USER') }}" diff --git a/modules/artifactory/provision/main.yml b/modules/artifactory/provision/main.yml new file mode 100644 index 0000000..af32c40 --- /dev/null +++ b/modules/artifactory/provision/main.yml @@ -0,0 +1,85 @@ +- name: Artifactory IaC + hosts: all + gather_facts: false + vars: + license_string: "{{ lookup('amazon.aws.aws_secret', 'artifactory') | from_json}}" + admin_user: "{{ license_string.username }}" + admin_password: "{{ license_string.password }}" + env: "{{ ENV }}" + + tasks: + - name: Wait until the instance is ready + ansible.builtin.wait_for_connection: + + - name: Gather facts for first time + ansible.builtin.setup: + + - name: Artifactory repository added + become: true + ansible.builtin.yum_repository: + name: artifactory-pro-rpms + description: artifactory Pro + baseurl: https://releases.jfrog.io/artifactory/artifactory-pro-rpms/ + + - name: Artifactory Pro installed + become: true + ansible.builtin.dnf: + name: jfrog-artifactory-pro + update_cache: true + disable_gpg_check: true + state: present + + - name: Create /opt/jfrog/artifactory/var/etc/access directory + become: true + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: artifactory + group: artifactory + mode: u=rwx,g=rx + with_items: + - /opt/jfrog/artifactory/var/etc/access + - /opt/jfrog/artifactory/var/etc/artifactory + + - name: Re-configure built-in administrator creds + become: true + ansible.builtin.template: + src: "{{ playbook_dir }}/templates/bootstrap.creds.j2" + dest: /opt/jfrog/artifactory/var/etc/access/bootstrap.creds + owner: artifactory + group: artifactory + mode: u=rw + + - name: Set license for non STAGE environment + ansible.builtin.set_fact: + license: "{{ license_string.artifactory_license1 }}" + when: env != "stage" + + - name: Set license for STAGE environment + ansible.builtin.set_fact: + license: "{{ license_string.artifactory_license2 }}" + when: env == "stage" + + - name: Use license strings + become: true + ansible.builtin.copy: + dest: /opt/jfrog/artifactory/var/etc/artifactory/artifactory.lic + content: "{{ license }}" + mode: "0644" + + - name: Artifactory service is enabled and running + become: true + ansible.builtin.systemd: + state: started + enabled: true + name: artifactory + + - name: Make sure artifactory is healthy + ansible.builtin.uri: + url: http://{{ inventory_hostname }}:8082/router/api/v1/system/health + timeout: 130 + status_code: 200 + register: result + until: result is succeeded + retries: 25 + delay: 5 diff --git a/modules/artifactory/provision/requirements.yml b/modules/artifactory/provision/requirements.yml new file mode 100644 index 0000000..b47bdd0 --- /dev/null +++ b/modules/artifactory/provision/requirements.yml @@ -0,0 +1,5 @@ +--- +collections: + - name: amazon.aws + version: 6.4.0 + type: galaxy diff --git a/modules/artifactory/provision/templates/bootstrap.creds.j2 b/modules/artifactory/provision/templates/bootstrap.creds.j2 new file mode 100644 index 0000000..15c7437 --- /dev/null +++ b/modules/artifactory/provision/templates/bootstrap.creds.j2 @@ -0,0 +1 @@ +{{ admin_user }}@*={{ admin_password }} diff --git a/modules/artifactory/variable.tf b/modules/artifactory/variable.tf new file mode 100644 index 0000000..0cee474 --- /dev/null +++ b/modules/artifactory/variable.tf @@ -0,0 +1,69 @@ +variable "aws_region" { + type = string + default = "eu-central-1" + description = "AWS region to launch servers." +} + +variable "artifactory_security_group_name" { + type = string + default = "tf-artifactory-security-group" +} + +variable "secretsmanager_policy" { + type = string + default = "tf-secretsmanager-policy" + description = "Consume Secrets from Secrets Manager" +} + +variable "artifactory_iam_role_name" { + type = string + default = "tf-artifactory-iam-role-name" + description = "Artifactory instance IAM Role name" +} + +variable "artifactory_iam_instance_profile" { + type = string + default = "tf-artifactory-iam-instance-profile" + description = "Artifactory IAM instance profile" +} + +variable "ssh_key_name" { + type = string + default = "tf-artifactory-ssh-key" +} + +variable "ssh_private_file_name" { + type = string + default = "/workspace/tf-artifactory-ssh-key.pem" +} + + +variable "artifactory_instance_type" { + type = string + default = "m5.large" + description = "Artifactory EC2 instance type" +} + +variable "artifactory_server_name" { + type = string + default = "artifactory" + description = "Provide artifactory server name to be used in Nginx. e.g artifactory for artifactory.jfrog.team" +} + +variable "user" { + type = string + default = "" +} + +variable "aws_profile" { + type = string + default = "default" +} + +variable "tags" { + type = map(string) + default = { + "Project" = "platform-engineering" + } + description = "Resource tags" +}