diff --git a/README.md b/README.md index 346297d..fbe530d 100644 --- a/README.md +++ b/README.md @@ -71,17 +71,14 @@ Project name: My Project Name Project slug [my-project-name]: Service slug [frontend]: Project dirname (frontend, myprojectname) [frontend]: myprojectname -Deploy type (digitalocean-k8s, other-k8s) [digitalocean-k8s]: Terraform backend (gitlab, terraform-cloud) [terraform-cloud]: Terraform host name [app.terraform.io]: Terraform Cloud User token: Terraform Organization: my-organization-name Do you want to create Terraform Cloud Organization 'my-organization-name'? [y/N]: -Choose the environments distribution: - 1 - All environments share the same stack (Default) - 2 - Dev and Stage environments share the same stack, Prod has its own - 3 - Each environment has its own stack - (1, 2, 3) [1]: +Cluster slug hosting the 'development' environment [dev]: +Cluster slug hosting the 'staging' environment [dev]: +Cluster slug hosting the 'production' environment [main]: Development environment complete URL [https://dev.my-project-name.com/]: Staging environment complete URL [https://stage.my-project-name.com/]: Production environment complete URL [https://www.my-project-name.com/]: @@ -98,6 +95,8 @@ Initializing the frontend service: ...creating the Terraform Cloud resources ``` +The generated service deploys via the [Minos service module](https://gitlab.com/20tab-open/minos) using the `registry.gitlab.com/20tab-open/minos/service:latest` image and the OpenTofu GitLab Components (`opentofu/apply@3.11.0`). Each environment (`development`, `staging`, `production`) is mapped to a Kubernetes cluster slug via the prompts above. + ## 🗒️ Arguments The following arguments can be appended to the Docker and shell commands @@ -142,13 +141,6 @@ The following arguments can be appended to the Docker and shell commands ### 📐 Architecture -#### Deploy type - -| Description | Argument | -| ----------------------- | ------------------------------------ | -| DigitalOcean Kubernetes | `--deployment-type=digitalocean-k8s` | -| Other Kubernetes | `--deployment-type=other-k8s` | - #### Terraform backend | Name | Argument | @@ -170,14 +162,9 @@ The following arguments can be appended to the Docker and shell commands Disabled args `--terraform-cloud-organization-create-skip` -#### Environment distribution +#### Env-to-cluster mapping -Choose the environments distribution: -Value | Description | Argument -------------- | ------------- | ------------- -1 | All environments share the same stack (Default) | `--environment-distribution=1` -2 | Dev and Stage environments share the same stack, Prod has its own | `--environment-distribution=2` -3 | Each environment has its own stack | `--environment-distribution=3` +Each of the three environments (`development`, `staging`, `production`) is mapped to a Kubernetes cluster slug. Defaults: `dev` for development and staging, `main` for production. The mapping can be set non-interactively via repeated `--env-to-cluster` flags or supplied programmatically as a `dict[str, str]`. #### Project Domain diff --git a/bootstrap/collector.py b/bootstrap/collector.py index ddc4dc7..d74fa78 100644 --- a/bootstrap/collector.py +++ b/bootstrap/collector.py @@ -9,13 +9,13 @@ from slugify import slugify from bootstrap.constants import ( - DEPLOYMENT_TYPE_CHOICES, - DEPLOYMENT_TYPE_DIGITALOCEAN, - DEPLOYMENT_TYPE_OTHER, - ENVIRONMENTS_DISTRIBUTION_CHOICES, - ENVIRONMENTS_DISTRIBUTION_DEFAULT, - ENVIRONMENTS_DISTRIBUTION_PROMPT, + ENV_NAMES, + ENV_TO_CLUSTER_DEFAULT, GITLAB_URL_DEFAULT, + MINOS_SERVICE_IMAGE, + NODE_VERSION_DEFAULT, + OPENTOFU_COMPONENT_VERSION, + OPENTOFU_VERSION, TERRAFORM_BACKEND_CHOICES, TERRAFORM_BACKEND_TFC, ) @@ -42,7 +42,6 @@ class Collector: service_slug: str | None = None internal_backend_url: str | None = None internal_service_port: int | None = None - deployment_type: str | None = None terraform_backend: str | None = None terraform_cloud_hostname: str | None = None terraform_cloud_token: str | None = None @@ -51,7 +50,7 @@ class Collector: terraform_cloud_admin_email: str | None = None vault_token: str | None = None vault_url: str | None = None - environments_distribution: str | None = None + env_to_cluster: dict[str, str] | None = None project_url_dev: str | None = None project_url_stage: str | None = None project_url_prod: str | None = None @@ -62,6 +61,10 @@ class Collector: gitlab_url: str | None = None gitlab_token: str | None = None gitlab_namespace_path: str | None = None + node_version: str | None = None + minos_service_image: str | None = None + opentofu_component_version: str | None = None + opentofu_version: str | None = None uid: int | None = None gid: int | None = None terraform_dir: Path | None = None @@ -81,11 +84,11 @@ def collect(self): self.set_use_redis() self.set_terraform() self.set_vault() - self.set_deployment_type() - self.set_environments_distribution() + self.set_env_to_cluster() self.set_project_urls() self.set_sentry() self.set_gitlab() + self.set_versions() def set_project_slug(self): """Set the project slug option.""" @@ -189,27 +192,15 @@ def set_vault(self): ) self.vault_url = validate_or_prompt_url("Vault address", self.vault_url) - def set_deployment_type(self): - """Set the deployment type option.""" - if self.deployment_type not in DEPLOYMENT_TYPE_CHOICES: - self.deployment_type = click.prompt( - "Deploy type", - default=DEPLOYMENT_TYPE_DIGITALOCEAN, - type=click.Choice(DEPLOYMENT_TYPE_CHOICES, case_sensitive=False), - ).lower() - - def set_environments_distribution(self): - """Set the environments distribution option.""" - # TODO: forcing a single stack when deployment is `k8s-other` should be removed, - # and `set_deployment_type` merged with `set_deployment` - if self.deployment_type == DEPLOYMENT_TYPE_OTHER: - self.environments_distribution = "1" - elif self.environments_distribution not in ENVIRONMENTS_DISTRIBUTION_CHOICES: - self.environments_distribution = click.prompt( - ENVIRONMENTS_DISTRIBUTION_PROMPT, - default=ENVIRONMENTS_DISTRIBUTION_DEFAULT, - type=click.Choice(ENVIRONMENTS_DISTRIBUTION_CHOICES), - ) + def set_env_to_cluster(self): + """Set the environment-to-cluster mapping (one cluster slug per environment).""" + self.env_to_cluster = self.env_to_cluster or {} + for env_name in ENV_NAMES: + if env_name not in self.env_to_cluster: + self.env_to_cluster[env_name] = click.prompt( + f"Cluster slug hosting the '{env_name}' environment", + default=ENV_TO_CLUSTER_DEFAULT[env_name], + ) def set_project_urls(self): """Set the project urls options.""" @@ -274,6 +265,21 @@ def set_gitlab(self): ) ) + def set_versions(self): + """Set the toolchain versions.""" + self.node_version = self.node_version or click.prompt( + "Node version", default=NODE_VERSION_DEFAULT + ) + self.minos_service_image = self.minos_service_image or click.prompt( + "Minos service image", default=MINOS_SERVICE_IMAGE + ) + self.opentofu_component_version = self.opentofu_component_version or click.prompt( + "OpenTofu CI component version", default=OPENTOFU_COMPONENT_VERSION + ) + self.opentofu_version = self.opentofu_version or click.prompt( + "OpenTofu version", default=OPENTOFU_VERSION + ) + def get_runner(self): """Get the bootstrap runner instance.""" return Runner( @@ -287,7 +293,6 @@ def get_runner(self): service_slug=self.service_slug, internal_backend_url=self.internal_backend_url, internal_service_port=self.internal_service_port, - deployment_type=self.deployment_type, terraform_backend=self.terraform_backend, terraform_cloud_hostname=self.terraform_cloud_hostname, terraform_cloud_token=self.terraform_cloud_token, @@ -296,7 +301,7 @@ def get_runner(self): terraform_cloud_admin_email=self.terraform_cloud_admin_email, vault_token=self.vault_token, vault_url=self.vault_url, - environments_distribution=self.environments_distribution, + env_to_cluster=self.env_to_cluster, project_url_dev=self.project_url_dev, project_url_stage=self.project_url_stage, project_url_prod=self.project_url_prod, @@ -307,6 +312,10 @@ def get_runner(self): gitlab_url=self.gitlab_url, gitlab_token=self.gitlab_token, gitlab_namespace_path=self.gitlab_namespace_path, + node_version=self.node_version, + minos_service_image=self.minos_service_image, + opentofu_component_version=self.opentofu_component_version, + opentofu_version=self.opentofu_version, terraform_dir=self.terraform_dir, logs_dir=self.logs_dir, ) diff --git a/bootstrap/constants.py b/bootstrap/constants.py index 85403b9..974f3c1 100644 --- a/bootstrap/constants.py +++ b/bootstrap/constants.py @@ -1,35 +1,6 @@ """Web project initialization CLI constants.""" -# Stacks - -# BEWARE: stack names must be suitable for inclusion in Vault paths - -DEV_STACK_NAME = "development" - -DEV_STACK_SLUG = "dev" - -STAGE_STACK_NAME = "staging" - -STAGE_STACK_SLUG = "stage" - -MAIN_STACK_NAME = "main" - -MAIN_STACK_SLUG = "main" - -STACKS_CHOICES = { - "1": [{"name": MAIN_STACK_NAME, "slug": MAIN_STACK_SLUG}], - "2": [ - {"name": DEV_STACK_NAME, "slug": DEV_STACK_SLUG}, - {"name": MAIN_STACK_NAME, "slug": MAIN_STACK_SLUG}, - ], - "3": [ - {"name": DEV_STACK_NAME, "slug": DEV_STACK_SLUG}, - {"name": STAGE_STACK_NAME, "slug": STAGE_STACK_SLUG}, - {"name": MAIN_STACK_NAME, "slug": MAIN_STACK_SLUG}, - ], -} - # Environments # BEWARE: environment names must be suitable for inclusion in Vault paths @@ -38,24 +9,15 @@ DEV_ENV_SLUG = "dev" -DEV_ENV_STACK_CHOICES: dict[str, str] = { - "1": MAIN_STACK_SLUG, -} - STAGE_ENV_NAME = "staging" STAGE_ENV_SLUG = "stage" -STAGE_ENV_STACK_CHOICES: dict[str, str] = { - "1": MAIN_STACK_SLUG, - "2": DEV_STACK_SLUG, -} - PROD_ENV_NAME = "production" PROD_ENV_SLUG = "prod" -PROD_ENV_STACK_CHOICES: dict[str, str] = {} +ENV_NAMES = [DEV_ENV_NAME, STAGE_ENV_NAME, PROD_ENV_NAME] # Env vars @@ -63,34 +25,44 @@ VAULT_TOKEN_ENV_VAR = "VAULT_TOKEN" -# Deployment type +# Terraform backend + +TERRAFORM_BACKEND_GITLAB = "gitlab" -DEPLOYMENT_TYPE_DIGITALOCEAN = "digitalocean-k8s" +TERRAFORM_BACKEND_TFC = "terraform-cloud" -DEPLOYMENT_TYPE_OTHER = "other-k8s" +TERRAFORM_BACKEND_CHOICES = [TERRAFORM_BACKEND_GITLAB, TERRAFORM_BACKEND_TFC] -DEPLOYMENT_TYPE_CHOICES = [DEPLOYMENT_TYPE_DIGITALOCEAN, DEPLOYMENT_TYPE_OTHER] +# GitLab -# Environments distribution +GITLAB_URL_DEFAULT = "https://gitlab.com" -ENVIRONMENTS_DISTRIBUTION_DEFAULT = "1" +# Clusters -ENVIRONMENTS_DISTRIBUTION_CHOICES = [ENVIRONMENTS_DISTRIBUTION_DEFAULT, "2", "3"] +CLUSTER_DEV_SLUG = "dev" -ENVIRONMENTS_DISTRIBUTION_PROMPT = """Choose the environments distribution: - 1 - All environments share the same stack (Default) - 2 - Dev and Stage environments share the same stack, Prod has its own - 3 - Each environment has its own stack -""" +CLUSTER_MAIN_SLUG = "main" -# Terraform backend +ENV_TO_CLUSTER_DEFAULT: dict[str, str] = { + DEV_ENV_NAME: CLUSTER_DEV_SLUG, + STAGE_ENV_NAME: CLUSTER_DEV_SLUG, + PROD_ENV_NAME: CLUSTER_MAIN_SLUG, +} -TERRAFORM_BACKEND_GITLAB = "gitlab" +# Vault -TERRAFORM_BACKEND_TFC = "terraform-cloud" +VAULT_SERVICE_ROLE = "service-gitlab-job" -TERRAFORM_BACKEND_CHOICES = [TERRAFORM_BACKEND_GITLAB, TERRAFORM_BACKEND_TFC] +# Minos -# GitLab +MINOS_SERVICE_IMAGE = "registry.gitlab.com/20tab-open/minos/service:latest" -GITLAB_URL_DEFAULT = "https://gitlab.com" +# OpenTofu + +OPENTOFU_COMPONENT_VERSION = "3.11.0" + +OPENTOFU_VERSION = "1.10.6" + +# Node + +NODE_VERSION_DEFAULT = "24.14.0" diff --git a/bootstrap/runner.py b/bootstrap/runner.py index 6a757cc..f97bc06 100644 --- a/bootstrap/runner.py +++ b/bootstrap/runner.py @@ -16,17 +16,14 @@ from bootstrap.constants import ( DEV_ENV_NAME, DEV_ENV_SLUG, - DEV_ENV_STACK_CHOICES, - DEV_STACK_SLUG, - MAIN_STACK_SLUG, + MINOS_SERVICE_IMAGE, + NODE_VERSION_DEFAULT, + OPENTOFU_COMPONENT_VERSION, + OPENTOFU_VERSION, PROD_ENV_NAME, PROD_ENV_SLUG, - PROD_ENV_STACK_CHOICES, - STACKS_CHOICES, STAGE_ENV_NAME, STAGE_ENV_SLUG, - STAGE_ENV_STACK_CHOICES, - STAGE_STACK_SLUG, TERRAFORM_BACKEND_TFC, ) from bootstrap.exceptions import BootstrapError @@ -54,8 +51,7 @@ class Runner: service_slug: str internal_backend_url: str | None internal_service_port: int - deployment_type: str - environments_distribution: str + env_to_cluster: dict[str, str] project_url_dev: str = "" project_url_stage: str = "" project_url_prod: str = "" @@ -74,12 +70,15 @@ class Runner: gitlab_url: str | None = None gitlab_namespace_path: str | None = None gitlab_token: str | None = None + node_version: str = NODE_VERSION_DEFAULT + minos_service_image: str = MINOS_SERVICE_IMAGE + opentofu_component_version: str = OPENTOFU_COMPONENT_VERSION + opentofu_version: str = OPENTOFU_VERSION uid: int | None = None gid: int | None = None terraform_dir: Path | None = None logs_dir: Path | None = None run_id: str = field(init=False) - stacks: list = field(init=False, default_factory=list) envs: list = field(init=False, default_factory=list) gitlab_variables: dict = field(init=False, default_factory=dict) tfvars: dict = field(init=False, default_factory=dict) @@ -92,45 +91,27 @@ def __post_init__(self): self.run_id = f"{time():.0f}" self.terraform_dir = self.terraform_dir or Path(f".terraform/{self.run_id}") self.logs_dir = self.logs_dir or Path(f".logs/{self.run_id}") - self.set_stacks() self.set_envs() self.collect_tfvars() self.collect_gitlab_variables() - def set_stacks(self): - """Set the stacks.""" - self.stacks = STACKS_CHOICES[self.environments_distribution] + def _env(self, name, slug, url, basic_auth_enabled): + host = (url or "").removeprefix("https://").removeprefix("http://").rstrip("/") + return { + "basic_auth_enabled": basic_auth_enabled, + "name": name, + "slug": slug, + "cluster_slug": self.env_to_cluster[name], + "host": host, + "url": url, + } def set_envs(self): """Set the envs.""" self.envs = [ - { - "basic_auth_enabled": True, - "name": DEV_ENV_NAME, - "slug": DEV_ENV_SLUG, - "stack_slug": DEV_ENV_STACK_CHOICES.get( - self.environments_distribution, DEV_STACK_SLUG - ), - "url": self.project_url_dev, - }, - { - "basic_auth_enabled": True, - "name": STAGE_ENV_NAME, - "slug": STAGE_ENV_SLUG, - "stack_slug": STAGE_ENV_STACK_CHOICES.get( - self.environments_distribution, STAGE_STACK_SLUG - ), - "url": self.project_url_stage, - }, - { - "basic_auth_enabled": False, - "name": PROD_ENV_NAME, - "slug": PROD_ENV_SLUG, - "stack_slug": PROD_ENV_STACK_CHOICES.get( - self.environments_distribution, MAIN_STACK_SLUG - ), - "url": self.project_url_prod, - }, + self._env(DEV_ENV_NAME, DEV_ENV_SLUG, self.project_url_dev, True), + self._env(STAGE_ENV_NAME, STAGE_ENV_SLUG, self.project_url_stage, True), + self._env(PROD_ENV_NAME, PROD_ENV_SLUG, self.project_url_prod, False), ] def register_gitlab_variable( @@ -210,19 +191,21 @@ def collect_tfvars(self): self.register_environment_tfvars( ("environment", env["name"]), ("project_url", env["url"]), - ("stack_slug", env["stack_slug"]), + ("cluster_slug", env["cluster_slug"]), env_slug=env["slug"], ) - def register_vault_environment_secret(self, env_name, secret_name, secret_data): - """Register a Vault environment secret locally.""" - self.vault_secrets[f"envs/{env_name}/{secret_name}"] = secret_data + def register_vault_service_secret(self, env_name, secret_name, secret_data): + """Register a service-scoped Vault secret at envs/{env}/{service_slug}/{secret_name}.""" + self.vault_secrets[ + f"envs/{env_name}/{self.service_slug}/{secret_name}" + ] = secret_data def collect_vault_environment_secrets(self, env_name): """Collect the Vault secrets for the given environment.""" - # Sentry env vars are used by the GitLab CI/CD - self.sentry_dsn and self.register_vault_environment_secret( - env_name, f"{self.service_slug}/sentry", {"sentry_dsn": self.sentry_dsn} + # Sentry DSN is read by the GitLab CI/CD via ci_sentry.sh + self.sentry_dsn and self.register_vault_service_secret( + env_name, "sentry", {"sentry_dsn": self.sentry_dsn} ) def collect_vault_secrets(self): @@ -235,18 +218,21 @@ def init_service(self): cookiecutter( os.path.dirname(os.path.dirname(__file__)), extra_context={ - "deployment_type": self.deployment_type, "internal_service_port": self.internal_service_port, "project_dirname": self.project_dirname, "project_name": self.project_name, "project_slug": self.project_slug, - "resources": {"envs": self.envs, "stacks": self.stacks}, + "resources": {"envs": self.envs}, "service_slug": self.service_slug, "terraform_backend": self.terraform_backend, "terraform_cloud_organization": self.terraform_cloud_organization, "tfvars": self.tfvars, "use_redis": self.use_redis and "true" or "false", "use_vault": self.vault_url and "true" or "false", + "node_version": self.node_version, + "minos_service_image": self.minos_service_image, + "opentofu_component_version": self.opentofu_component_version, + "opentofu_version": self.opentofu_version, }, output_dir=self.output_dir, no_input=True, diff --git a/cookiecutter.json b/cookiecutter.json index beab3a5..187f4a3 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -4,36 +4,33 @@ "service_slug": "frontend", "project_dirname": "frontend", "internal_service_port": "3000", - "deployment_type": ["digitalocean-k8s", "other-k8s"], "terraform_backend": "gitlab", "terraform_cloud_organization": null, "use_redis": "false", "use_vault": "false", - "environment_distribution": "1", + "node_version": "24.14.0", + "minos_service_image": "registry.gitlab.com/20tab-open/minos/service:latest", + "opentofu_component_version": "3.11.0", + "opentofu_version": "1.10.6", "resources": { - "stacks": [ - [ - { - "name": "main", - "slug": "main" - } - ] - ], "envs": [ { "name": "development", "slug": "dev", - "stack_slug": "main" + "cluster_slug": "main", + "host": "" }, { "name": "staging", "slug": "stage", - "stack_slug": "main" + "cluster_slug": "main", + "host": "" }, { "name": "production", "slug": "prod", - "stack_slug": "main" + "cluster_slug": "main", + "host": "" } ] }, diff --git a/start.py b/start.py index 4460b3b..4458b45 100755 --- a/start.py +++ b/start.py @@ -7,8 +7,6 @@ from bootstrap.collector import Collector from bootstrap.constants import ( - DEPLOYMENT_TYPE_CHOICES, - ENVIRONMENTS_DISTRIBUTION_CHOICES, GITLAB_TOKEN_ENV_VAR, VAULT_TOKEN_ENV_VAR, ) @@ -33,10 +31,6 @@ @click.option("--service-slug", callback=slugify_option) @click.option("--internal-backend-url") @click.option("--internal-service-port", default=3000, type=int) -@click.option( - "--deployment-type", - type=click.Choice(DEPLOYMENT_TYPE_CHOICES, case_sensitive=False), -) @click.option("--terraform-backend") @click.option("--terraform-cloud-hostname") @click.option("--terraform-cloud-token") @@ -49,9 +43,6 @@ @click.option("--terraform-cloud-admin-email") @click.option("--vault-token", envvar=VAULT_TOKEN_ENV_VAR) @click.option("--vault-url") -@click.option( - "--environments-distribution", type=click.Choice(ENVIRONMENTS_DISTRIBUTION_CHOICES) -) @click.option("--project-url-dev") @click.option("--project-url-stage") @click.option("--project-url-prod") diff --git a/tests/test_collector.py b/tests/test_collector.py index 61a2e54..fd7986c 100644 --- a/tests/test_collector.py +++ b/tests/test_collector.py @@ -248,73 +248,35 @@ def test_vault_from_input_and_options(self): self.assertEqual(collector.vault_url, "https://vault.test.com") self.assertEqual(len(mocked_confirm.mock_calls), 1) - def test_deployment_type_from_default(self): - """Test collecting the deployment type from its default value.""" - collector = Collector( - project_name="project_name", - ) - self.assertIsNone(collector.deployment_type) - with mock_input(""): - collector.set_deployment_type() - self.assertEqual(collector.deployment_type, "digitalocean-k8s") - - def test_deployment_type_from_input(self): - """Test collecting the deployment type from user input.""" - collector = Collector( - project_name="project_name", - ) - self.assertIsNone(collector.deployment_type) - with mock_input("bad-deployment-type", "yet-another-bad-value", "other-k8s"): - collector.set_deployment_type() - self.assertEqual(collector.deployment_type, "other-k8s") - - def test_deployment_type_from_options(self): - """Test collecting the deployment type from the collected options.""" - collector = Collector(project_name="project_name", deployment_type="other-k8s") - self.assertEqual(collector.deployment_type, "other-k8s") - with mock.patch("bootstrap.collector.click.prompt") as mocked_prompt: - collector.set_deployment_type() - self.assertEqual(collector.deployment_type, "other-k8s") - mocked_prompt.assert_not_called() - - def test_environments_distribution_for_other_k8s_deployment(self): - """Test collecting the environments distribution for other-k8s deployment.""" - collector = Collector(project_name="project_name", deployment_type="other-k8s") - self.assertIsNone(collector.environments_distribution) - with mock.patch("bootstrap.collector.click.prompt") as mocked_prompt: - collector.set_environments_distribution() - self.assertEqual(collector.environments_distribution, "1") - mocked_prompt.assert_not_called() - - def test_environments_distribution_from_default(self): - """Test collecting the environments distribution from its default value.""" - collector = Collector( - project_name="project_name", + def test_env_to_cluster_from_default(self): + """Test collecting the env-to-cluster mapping from default values.""" + collector = Collector(project_name="project_name") + self.assertIsNone(collector.env_to_cluster) + with mock_input("", "", ""): + collector.set_env_to_cluster() + self.assertEqual( + collector.env_to_cluster, + {"development": "dev", "staging": "dev", "production": "main"}, ) - self.assertIsNone(collector.environments_distribution) - with mock_input(""): - collector.set_environments_distribution() - self.assertEqual(collector.environments_distribution, "1") - def test_environments_distribution_from_input(self): - """Test collecting the environments distribution from user input.""" - collector = Collector( - project_name="project_name", + def test_env_to_cluster_from_input(self): + """Test collecting the env-to-cluster mapping from user input.""" + collector = Collector(project_name="project_name") + self.assertIsNone(collector.env_to_cluster) + with mock_input("alpha", "beta", "gamma"): + collector.set_env_to_cluster() + self.assertEqual( + collector.env_to_cluster, + {"development": "alpha", "staging": "beta", "production": "gamma"}, ) - self.assertIsNone(collector.environments_distribution) - with mock_input("one", "yet-another-bad-value", "3"): - collector.set_environments_distribution() - self.assertEqual(collector.environments_distribution, "3") - def test_environments_distribution_from_options(self): - """Test collecting the environments distribution from the collected options.""" - collector = Collector( - project_name="project_name", environments_distribution="2" - ) - self.assertEqual(collector.environments_distribution, "2") + def test_env_to_cluster_from_options(self): + """Test collecting the env-to-cluster mapping pre-populated by options.""" + preset = {"development": "x", "staging": "y", "production": "z"} + collector = Collector(project_name="project_name", env_to_cluster=preset) with mock.patch("bootstrap.collector.click.prompt") as mocked_prompt: - collector.set_environments_distribution() - self.assertEqual(collector.environments_distribution, "2") + collector.set_env_to_cluster() + self.assertEqual(collector.env_to_cluster, preset) mocked_prompt.assert_not_called() def test_set_project_urls_from_default(self): @@ -478,8 +440,7 @@ def test_launch_runner(self): def test_get_runner(self): """Test getting the runner.""" collector = Collector( - deployment_type="digitalocean-k8s", - environments_distribution="1", + env_to_cluster={"development": "dev", "staging": "dev", "production": "main"}, internal_service_port=8000, project_dirname="project_dirname", project_name="Test Project", @@ -490,11 +451,17 @@ def test_get_runner(self): service_slug="django", terraform_backend="terraform-cloud", use_redis=False, + node_version="24.14.0", + minos_service_image="registry.gitlab.com/20tab-open/minos/service:latest", + opentofu_component_version="3.11.0", + opentofu_version="1.10.6", ) collector._service_dir = Path(".") runner = collector.get_runner() - self.assertEqual(runner.deployment_type, "digitalocean-k8s") - self.assertEqual(runner.environments_distribution, "1") + self.assertEqual( + runner.env_to_cluster, + {"development": "dev", "staging": "dev", "production": "main"}, + ) self.assertEqual(runner.internal_service_port, 8000) self.assertEqual(runner.project_dirname, "project_dirname") self.assertEqual(runner.project_name, "Test Project") @@ -576,11 +543,11 @@ def test_collect(self): collector.set_use_redis = mock.MagicMock() collector.set_terraform = mock.MagicMock() collector.set_vault = mock.MagicMock() - collector.set_deployment_type = mock.MagicMock() - collector.set_environments_distribution = mock.MagicMock() + collector.set_env_to_cluster = mock.MagicMock() collector.set_project_urls = mock.MagicMock() collector.set_sentry = mock.MagicMock() collector.set_gitlab = mock.MagicMock() + collector.set_versions = mock.MagicMock() collector.collect() collector.set_project_slug.assert_called_once() collector.set_project_dirname.assert_called_once() @@ -588,8 +555,8 @@ def test_collect(self): collector.set_use_redis.assert_called_once() collector.set_terraform.assert_called_once() collector.set_vault.assert_called_once() - collector.set_deployment_type.assert_called_once() - collector.set_environments_distribution.assert_called_once() + collector.set_env_to_cluster.assert_called_once() collector.set_project_urls.assert_called_once() collector.set_sentry.assert_called_once() collector.set_gitlab.assert_called_once() + collector.set_versions.assert_called_once() diff --git a/{{cookiecutter.project_dirname}}/.gitlab-ci.yml b/{{cookiecutter.project_dirname}}/.gitlab-ci.yml index 3ee3cb3..3d36214 100644 --- a/{{cookiecutter.project_dirname}}/.gitlab-ci.yml +++ b/{{cookiecutter.project_dirname}}/.gitlab-ci.yml @@ -1,10 +1,17 @@ +{% set env_dev = cookiecutter.resources.envs|selectattr("slug", "equalto", "dev")|first %}{% set env_stage = cookiecutter.resources.envs|selectattr("slug", "equalto", "stage")|first %}{% set env_prod = cookiecutter.resources.envs|selectattr("slug", "equalto", "prod")|first %}include: + - component: ${CI_SERVER_FQDN}/components/opentofu/apply@{{ cookiecutter.opentofu_component_version }} + inputs: + as: .apply + version: {{ cookiecutter.opentofu_component_version }} + opentofu_version: {{ cookiecutter.opentofu_version }} + no_plan: true + stages: - Test - Pact-publish - Pact-check - Build - Report - - E2E - Deploy - Pact-tag - Sentry @@ -15,13 +22,23 @@ variables: PACT_CONSUMER_NAME: {{ cookiecutter.project_slug }}-{{ cookiecutter.service_slug }} PROJECT_SLUG: {{ cookiecutter.project_slug }} SENTRY_PROJECT_NAME: {{ cookiecutter.project_slug }}-{{ cookiecutter.service_slug }} + SERVICE_SLUG: {{ cookiecutter.service_slug }} + VAULT_ROLE: service-gitlab-job VERSION_BEFORE_REF: ${CI_COMMIT_BEFORE_SHA} - VERSION_REF: ${CI_COMMIT_SHA} cache: paths: - node_modules/ -{% with env=cookiecutter.resources.envs[0] %} + +workflow: + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + when: never + - when: always + +# [Environments] +# ----------------------------------------------------------------------------- + .development: rules: &development-rules - &pipeline-push-rule @@ -30,79 +47,60 @@ cache: - &development-rule if: $CI_COMMIT_BRANCH == "develop" variables: - ENV_SLUG: {{ env.slug }} - STACK_SLUG: {{ env.stack_slug }} - VAULT_ROLE: {{ cookiecutter.service_slug }}-{{ env.slug }} + CLUSTER_SLUG: {{ env_dev.cluster_slug }} environment: - name: {{ env.name }}{% if env.url %} - url: {{ env.url }}{% endif %} -{% endwith %}{% with env=cookiecutter.resources.envs[1] %} + name: development + url: {{ env_dev.url }} + .staging: rules: &staging-rules - <<: *pipeline-push-rule - &staging-rule if: $CI_COMMIT_BRANCH == "main" variables: - ENV_SLUG: {{ env.slug }} - STACK_SLUG: {{ env.stack_slug }} - VAULT_ROLE: {{ cookiecutter.service_slug }}-{{ env.slug }} + CLUSTER_SLUG: {{ env_stage.cluster_slug }} environment: - name: {{ env.name }}{% if env.url %} - url: {{ env.url }}{% endif %} -{% endwith %}{% with env=cookiecutter.resources.envs[2] %} + name: staging + url: {{ env_stage.url }} + .production: rules: &production-rules - <<: *pipeline-push-rule - &production-rule if: $CI_COMMIT_TAG variables: - ENV_SLUG: {{ env.slug }} - STACK_SLUG: {{ env.stack_slug }} - VAULT_ROLE: {{ cookiecutter.service_slug }}-{{ env.slug }} + CLUSTER_SLUG: {{ env_prod.cluster_slug }} environment: - name: {{ env.name }}{% if env.url %} - url: {{ env.url }}{% endif %} -{% endwith %} + name: production + url: {{ env_prod.url }} + +# [Pre] +# ----------------------------------------------------------------------------- .sentry: stage: .pre - image: docker:20 - services: - - docker:20-dind{% if cookiecutter.use_vault == "true" %} + image: + name: getsentry/sentry-cli:latest + entrypoint: [] id_tokens: VAULT_ID_TOKEN: - aud: ${VAULT_ADDR}{% endif %} - script: - - > - docker run --rm - -v ${PWD}:${PWD} - -w ${PWD} - -e CI_ENVIRONMENT_NAME{% if cookiecutter.use_vault == "true" %} - -e ENV_NAME=${CI_ENVIRONMENT_NAME}{% endif %} - -e PROJECT_DIR=${CI_PROJECT_DIR} - -e PROJECT_SLUG - -e RELEASE_END - -e RELEASE_START{% if cookiecutter.use_vault == "false" %} - -e SENTRY_AUTH_TOKEN - -e SENTRY_DSN{% endif %} - -e SENTRY_ORG - -e SENTRY_PROJECT_NAME - -e SENTRY_URL{% if cookiecutter.use_vault == "true" %} - -e SERVICE_SLUG={{ cookiecutter.service_slug }} - -e VAULT_ADDR - -e VAULT_ID_TOKEN - -e VAULT_ROLE{% endif %} - -e VERSION_REF - --entrypoint="" - getsentry/sentry-cli:latest ./scripts/ci_sentry.sh ${SENTRY_CMD} + aud: ${VAULT_ADDR} + variables: + ENV_NAME: ${CI_ENVIRONMENT_SLUG} + PROJECT_DIR: ${CI_PROJECT_DIR} + before_script: + - source ./scripts/ci_sentry.sh .sentry_release: extends: - .sentry - variables: - SENTRY_CMD: release - before_script: + script: - RELEASE_START=$(date +%s) + - > + sentry-cli releases new "${CI_COMMIT_SHA}" + --log-level=debug --project "${SENTRY_PROJECT_NAME}" + - sentry-cli releases set-commits "${CI_COMMIT_SHA}" --auto --ignore-missing + - sentry-cli releases finalize "${CI_COMMIT_SHA}" sentry_release_development: extends: @@ -110,7 +108,7 @@ sentry_release_development: - .sentry_release rules: - &sentry-rule - if: $SENTRY_ENABLED != "true" + if: $SENTRY_ENABLED != "true" || $SKIP_DEPLOY == "true" when: never - *development-rules @@ -130,62 +128,48 @@ sentry_release_production: - <<: *sentry-rule - *production-rules +# [Test] +# ----------------------------------------------------------------------------- + test: stage: Test - image: docker:20 - services: - - docker:20-dind + image: node:{{ cookiecutter.node_version }}-bullseye-slim needs: [] variables: - COMPOSE_FILE: docker-compose.yaml:docker-compose/test.yaml - COMPOSE_PROJECT_NAME: "${CI_PROJECT_PATH_SLUG}-${CI_JOB_NAME}-${CI_JOB_ID}" - SERVICE_CONTAINER_NAME: "${CI_PROJECT_PATH_SLUG}-${CI_JOB_NAME}-${CI_JOB_ID}_frontend" - SERVICE_DOCKER_FILE: "docker/test.Dockerfile" - SERVICE_IMAGE_NAME: "gitlabci_{{ cookiecutter.project_slug }}_{{ cookiecutter.service_slug }}" - SERVICE_IMAGE_TAG: "${CI_JOB_NAME}-${CI_JOB_ID}" - before_script: - - mkdir pacts + NEXT_TELEMETRY_DISABLED: 1 + PACT_DO_NOT_TRACK: 1 + TZ: "Europe/Rome" script: - - docker-compose build --quiet consumer - - docker-compose run --name ${SERVICE_CONTAINER_NAME} consumer - after_script: - - docker cp ${SERVICE_CONTAINER_NAME}:/app/coverage . - - docker cp ${SERVICE_CONTAINER_NAME}:/app/pacts . - - docker-compose down --volumes --remove-orphans + - corepack enable && corepack prepare yarn@stable --activate && yarn set version stable + - yarn install + - yarn ci:contract-test && yarn ci:unit-test coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/ artifacts: paths: - - coverage - - pacts + - .artifacts/ reports: junit: - - junit.xml + - .artifacts/junit.xml expire_in: 1 day + rules: + - if: $SKIP_TEST == "true" + when: never + - when: always + +# [Pact Publish] +# ----------------------------------------------------------------------------- .pact: image: - name: docker:20 - services: - - docker:20-dind{% if cookiecutter.use_vault == "true" %} + name: pactfoundation/pact-cli:latest + entrypoint: [] id_tokens: VAULT_ID_TOKEN: - aud: ${VAULT_ADDR}{% endif %} - script: - - > - docker run --rm - -v ${PWD}:${PWD} - -w ${PWD}{% if cookiecutter.use_vault == "true" %} - -e ENV_SLUG{% else %} - -e PACT_BROKER_BASE_URL - -e PACT_BROKER_PASSWORD - -e PACT_BROKER_USERNAME{% endif %} - -e PROJECT_SLUG{% if cookiecutter.use_vault == "true" %} - -e VAULT_ADDR - -e VAULT_ID_TOKEN{% endif %} - --entrypoint="" - pactfoundation/pact-cli:latest-node14 ./scripts/ci_pact.sh ${PACT_CMD} - -publish: + aud: ${VAULT_ADDR} + before_script: + - source ./scripts/ci_pact.sh + +pact_publish: extends: - .pact stage: Pact-publish @@ -198,42 +182,51 @@ publish: if: $CI_PIPELINE_SOURCE != "push" when: never - when: always - before_script: + script: - if [ "${CI_COMMIT_BRANCH}" ]; then PACT_CONSUMER_TAG="branch:${CI_COMMIT_BRANCH}"; else PACT_CONSUMER_TAG="tag:${CI_COMMIT_TAG}"; fi - > - export PACT_CMD="publish ./pacts - --consumer-app-version=${VERSION_REF} - --tag=${PACT_CONSUMER_TAG}" + pact-broker publish .artifacts/pacts + --consumer-app-version="${CI_COMMIT_SHA}" + --tag="${PACT_CONSUMER_TAG}" + +# [Report] +# ----------------------------------------------------------------------------- pages: stage: Report image: busybox needs: ["test"] rules: + - if: $SKIP_TEST == "true" + when: never - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH script: - - mv coverage/tests/lcov-report/* public + - mkdir -p public + - mv .artifacts/coverage/lcov-report/* public artifacts: paths: - public +# [Can I Deploy] +# ----------------------------------------------------------------------------- + .can-i-deploy: extends: - .pact stage: Pact-check allow_failure: false - before_script: + script: - > - export PACT_CMD="can-i-deploy + pact-broker can-i-deploy --pacticipant ${PACT_CONSUMER_NAME} - --version ${VERSION_REF} - --to env:${ENV_SLUG}" + --version "${CI_COMMIT_SHA}" + --to "${CI_ENVIRONMENT_SLUG}" can-i-deploy_development: extends: - .development - .can-i-deploy - needs: ["publish"] + needs: ["pact_publish"] rules: - <<: *skip-pact-rule - <<: *push-only @@ -243,7 +236,7 @@ can-i-deploy_staging: extends: - .staging - .can-i-deploy - needs: ["publish"] + needs: ["pact_publish"] rules: - <<: *skip-pact-rule - <<: *push-only @@ -253,30 +246,44 @@ can-i-deploy_production: extends: - .production - .can-i-deploy - needs: ["publish"] + needs: ["pact_publish"] rules: - <<: *skip-pact-rule - <<: *push-only - *production-rules +# [Build] +# ----------------------------------------------------------------------------- + .build: stage: Build - image: docker:20 - services: - - docker:20-dind + image: quay.io/buildah/stable + variables: + BUILDAH_FORMAT: docker + BUILDAH_ISOLATION: chroot + NEXT_PUBLIC_PROJECT_URL: $CI_ENVIRONMENT_URL + REGISTRY_IMAGE: "${CI_REGISTRY}/${CI_PROJECT_PATH}/${CI_ENVIRONMENT_SLUG}:${CI_COMMIT_SHA}" + SENTRY_ENVIRONMENT: "${CI_ENVIRONMENT_NAME}" before_script: - - export DOCKER_CONFIG=${PWD}/.dockerconfig - - docker login --username "${CI_REGISTRY_USER}" --password "${CI_REGISTRY_PASSWORD}" "${CI_REGISTRY}" + - echo "${CI_REGISTRY_PASSWORD}" | buildah login "${CI_REGISTRY}" --username "${CI_REGISTRY_USER}" --password-stdin script: - - > - docker build - --tag=${CI_REGISTRY}/${CI_PROJECT_PATH}:${VERSION_REF} - --file=docker/remote.Dockerfile - --target=remote - --pull . - - docker push ${CI_REGISTRY}/${CI_PROJECT_PATH}:${VERSION_REF} + - | + buildah bud \ + --build-arg NEXT_PUBLIC_PROJECT_URL \ + --build-arg SENTRY_DSN \ + --build-arg SENTRY_ENVIRONMENT \ + --build-arg SENTRY_ORG \ + --build-arg SENTRY_PROJECT_NAME \ + --build-arg SENTRY_TRACES_SAMPLE_RATE \ + --build-arg SENTRY_URL \ + --file docker/remote.Dockerfile \ + --pull \ + --secret id=SENTRY_AUTH_TOKEN,env=SENTRY_AUTH_TOKEN \ + --tag "${REGISTRY_IMAGE}" \ + --target remote . + - buildah push "${REGISTRY_IMAGE}" after_script: - - docker logout ${CI_REGISTRY} + - buildah logout "${CI_REGISTRY}" build_development: extends: @@ -286,6 +293,7 @@ build_development: - job: can-i-deploy_development optional: true - job: test + optional: true build_staging: extends: @@ -295,6 +303,7 @@ build_staging: - job: can-i-deploy_staging optional: true - job: test + optional: true build_production: extends: @@ -304,180 +313,72 @@ build_production: - job: can-i-deploy_production optional: true - job: test + optional: true -.e2e: - stage: E2E - image: docker:20 - services: - - docker:20-dind - variables: - COMPOSE_PROJECT_NAME: "${CI_PROJECT_PATH_SLUG}-${CI_JOB_NAME}-${CI_JOB_ID}" - SERVICE_CONTAINER_NAME: "${CI_PROJECT_PATH_SLUG}-${CI_JOB_NAME}-${CI_JOB_ID}_e2e" - before_script: - - if [ "${PACT_ENABLED}" == "true" ]; then COMPOSE_PROFILES=pact; else COMPOSE_PROFILES=no-pact; fi - - export COMPOSE_PROFILES - - export CONSUMER_IMAGE=${CI_REGISTRY}/${CI_PROJECT_PATH}:${VERSION_REF} - - docker login --username "${CI_REGISTRY_USER}" --password "${CI_REGISTRY_PASSWORD}" "${CI_REGISTRY}" - script: - - docker-compose build --quiet consumer - - docker-compose up -d proxy - - docker-compose run --name ${SERVICE_CONTAINER_NAME} cypress - after_script: - - docker cp ${SERVICE_CONTAINER_NAME}:/app/cypress-outputs ./screenshots - - docker-compose down --volumes --remove-orphans - - docker logout ${CI_REGISTRY} - artifacts: - paths: - - screenshots - expire_in: 1 day - -e2e_development: - extends: - - .development - - .e2e - needs: ["test", "build_development"] - rules: - - &skip-e2e-rule - if: $SKIP_E2E == "true" - when: never - - <<: *push-only - - *development-rules - -e2e_staging: - extends: - - .staging - - .e2e - needs: ["test", "build_staging"] - rules: - - <<: *skip-e2e-rule - - <<: *push-only - - *staging-rules - -e2e_production: - extends: - - .production - - .e2e - needs: ["test", "build_production"] - rules: - - <<: *skip-e2e-rule - - <<: *push-only - - *production-rules - -e2e_manual: - rules: - - <<: *skip-e2e-rule - - <<: *push-only - - if: $CI_COMMIT_REF_PROTECTED == "true" - when: never - - when: manual - extends: - - .e2e - variables: - NEXT_PUBLIC_PROJECT_URL: 'https://proxy:8443' - REACT_ENVIRONMENT: 'production' - SERVICE_DOCKER_FILE: "docker/remote.Dockerfile" - COMPOSE_FILE: docker-compose.yaml:docker-compose/e2e.yaml:docker-compose/e2e-branch.yaml - needs: ["test"] +# [Deploy] +# ----------------------------------------------------------------------------- .deploy: stage: Deploy - image: - name: docker:20 - services: - - docker:20-dind{% if cookiecutter.use_vault == "true" %} + extends: + - .apply id_tokens: VAULT_ID_TOKEN: - aud: ${VAULT_ADDR}{% endif %} + aud: ${VAULT_ADDR} + image: {{ cookiecutter.minos_service_image }} variables: - TF_ROOT: ${CI_PROJECT_DIR}/terraform/{{ cookiecutter.deployment_type }} + GITLAB_TOFU_INIT_NO_RECONFIGURE: true + GITLAB_TOFU_ROOT_DIR: ${CI_PROJECT_DIR}/tofu + PROJECT_DIR: ${CI_PROJECT_DIR} + TF_CLOUD_HOSTNAME: app.terraform.io + TF_CLOUD_ORGANIZATION: {{ cookiecutter.terraform_cloud_organization }} + TF_VAR_registry_password: ${CI_DEPLOY_PASSWORD} + TF_VAR_registry_server: ${CI_REGISTRY} + TF_VAR_registry_username: ${CI_DEPLOY_USER} + TF_WORKSPACE: "${PROJECT_SLUG}_${SERVICE_SLUG}_${CI_ENVIRONMENT_SLUG}" + TOFU_BACKEND: terraform-cloud + TOFU_VAR_FILES: "common.tfvars ${CI_ENVIRONMENT_SLUG}/this.tfvars" + VAULT_SECRETS_PREFIX: "envs/${CI_ENVIRONMENT_SLUG}" + VAULT_SECRETS: "digitalocean" + VAULT_SERVICE_SECRETS: "shared-secrets.tftpl" before_script: - - export TF_VAR_service_container_image=${CI_REGISTRY_IMAGE}:${VERSION_REF} - script: - - > - docker run --rm - -u `id -u` - -v ${PWD}:${PWD} - -w ${PWD}{% if cookiecutter.terraform_backend == "gitlab" %} - -e CI_API_V4_URL - -e CI_COMMIT_SHA - -e CI_JOB_ID - -e CI_JOB_STAGE - -e CI_JOB_TOKEN - -e CI_PROJECT_ID - -e CI_PROJECT_NAME - -e CI_PROJECT_NAMESPACE - -e CI_PROJECT_PATH - -e CI_PROJECT_URL{% endif %} - -e ENV_SLUG - -e PROJECT_DIR=${CI_PROJECT_DIR} - -e PROJECT_SLUG - -e STACK_SLUG - -e TERRAFORM_BACKEND={{ cookiecutter.terraform_backend }} - -e TERRAFORM_EXTRA_VAR_FILE=${ENV_SLUG}.tfvars - -e TERRAFORM_VARS_DIR=${CI_PROJECT_DIR}/terraform/vars - -e TF_ROOT{% if cookiecutter.terraform_backend == "gitlab" %} - -e TF_STATE_NAME="env_${ENV_SLUG}"{% endif %}{% if cookiecutter.use_vault == "false" %}{% if cookiecutter.deployment_type == "digitalocean-k8s" %} - -e TF_VAR_digitalocean_token="${DIGITALOCEAN_TOKEN}"{% endif %} - -e TF_VAR_internal_backend_url="${INTERNAL_BACKEND_URL}" - -e TF_VAR_internal_service_port="${FRONTEND_SERVICE_PORT}"{% if cookiecutter.deployment_type == "other-k8s" %} - -e TF_VAR_kubernetes_cluster_ca_certificate="${KUBERNETES_CLUSTER_CA_CERTIFICATE}" - -e TF_VAR_kubernetes_host="${KUBERNETES_HOST}" - -e TF_VAR_kubernetes_token="${KUBERNETES_TOKEN}"{% endif %} - -e TF_VAR_project_url="${CI_ENVIRONMENT_URL}" - -e TF_VAR_sentry_dsn="${SENTRY_DSN}"{% endif %} - -e TF_VAR_service_container_image{% if cookiecutter.terraform_backend != "gitlab" %} - -e TF_WORKSPACE="{{ cookiecutter.project_slug }}_{{ cookiecutter.service_slug }}_environment_${ENV_SLUG}"{% endif %}{% if cookiecutter.terraform_backend == "terraform-cloud" and not cookiecutter.use_vault %} - -e TFC_TOKEN{% endif %}{% if cookiecutter.use_vault == "true" %} - -e VAULT_ADDR - -e VAULT_ID_TOKEN - -e VAULT_ROLE - -e VAULT_SECRETS="digitalocean k8s {{ cookiecutter.service_slug }}/extra {{ cookiecutter.service_slug }}/sentry" - -e VAULT_SECRETS_PREFIX="envs/${CI_ENVIRONMENT_NAME}" - -e VAULT_VERSION{% endif %} - registry.gitlab.com/gitlab-org/terraform-images/stable:latest ./scripts/deploy.sh - artifacts: - name: plan - reports: - terraform: ${TF_ROOT}/plan.json + - export TF_VAR_image=${CI_REGISTRY}/${CI_PROJECT_PATH}/${CI_ENVIRONMENT_SLUG}:${CI_COMMIT_SHA} + - export TF_CLI_ARGS="${TOFU_VAR_FILE_ARGS}" deploy_development: extends: - - .development - .deploy + - .development needs: - - job: e2e_development - optional: true - - job: build_development + - job: build_development deploy_staging: extends: - - .staging - .deploy + - .staging needs: - - job: e2e_staging - optional: true - - job: build_staging + - job: build_staging deploy_production: extends: - - .production - .deploy + - .production needs: - - job: e2e_production - optional: true - - job: build_production + - job: build_production .rollback: extends: - .deploy before_script: - - export TF_VAR_service_container_image=${CI_REGISTRY_IMAGE}:${VERSION_BEFORE_REF} + - export TF_VAR_image=${CI_REGISTRY}/${CI_PROJECT_PATH}/${CI_ENVIRONMENT_SLUG}:${VERSION_BEFORE_REF} + - export TF_CLI_ARGS="${TOFU_VAR_FILE_ARGS}" rollback_development: extends: - .development - .rollback - needs: ["deploy_development"] + needs: + - job: deploy_development rules: - <<: *pipeline-push-rule - <<: *development-rule @@ -488,7 +389,8 @@ rollback_staging: extends: - .staging - .rollback - needs: ["deploy_staging"] + needs: + - job: deploy_staging rules: - <<: *pipeline-push-rule - <<: *staging-rule @@ -499,26 +401,32 @@ rollback_production: extends: - .production - .rollback - needs: ["deploy_production"] + needs: + - job: deploy_production rules: - <<: *pipeline-push-rule - <<: *production-rule when: manual allow_failure: true +# [Pact Tag] +# ----------------------------------------------------------------------------- + .create-version-tag: extends: - .pact stage: Pact-tag - before_script: + script: - > - export PACT_CMD="create-version-tag - --pacticipant ${PACT_CONSUMER_NAME} - --version ${VERSION_REF} - --tag env:${ENV_SLUG}" + pact-broker create-version-tag + --pacticipant "${PACT_CONSUMER_NAME}" + --version "${CI_COMMIT_SHA}" + --tag "${CI_ENVIRONMENT_SLUG}" create-version-tag_development: - extends: .create-version-tag + extends: + - .development + - .create-version-tag needs: ["deploy_development"] rules: - <<: *skip-pact-rule @@ -526,7 +434,9 @@ create-version-tag_development: - *development-rules create-version-tag_staging: - extends: .create-version-tag + extends: + - .staging + - .create-version-tag needs: ["deploy_staging"] rules: - <<: *skip-pact-rule @@ -534,27 +444,35 @@ create-version-tag_staging: - *staging-rules create-version-tag_production: - extends: .create-version-tag + extends: + - .create-version-tag + - .production needs: ["deploy_production"] rules: - <<: *skip-pact-rule - <<: *push-only - *production-rules +# [Sentry] +# ----------------------------------------------------------------------------- + .sentry_deploy_success: extends: - .sentry - variables: - SENTRY_CMD: success stage: Sentry - before_script: + script: - RELEASE_END=$(date +%s) + - > + sentry-cli releases deploys "${CI_COMMIT_SHA}" + new --env "${ENV_NAME}" --time $((RELEASE_END-RELEASE_START)) sentry_success_development: extends: - .development - .sentry_deploy_success - needs: ["deploy_development"] + needs: + - job: deploy_development + optional: true rules: - <<: *sentry-rule - <<: *push-only @@ -565,7 +483,9 @@ sentry_success_staging: extends: - .staging - .sentry_deploy_success - needs: ["deploy_staging"] + needs: + - job: deploy_staging + optional: true rules: - <<: *sentry-rule - <<: *push-only @@ -576,7 +496,9 @@ sentry_success_production: extends: - .production - .sentry_deploy_success - needs: ["deploy_production"] + needs: + - job: deploy_production + optional: true rules: - <<: *sentry-rule - <<: *push-only @@ -586,36 +508,45 @@ sentry_success_production: .sentry_deploy_failure: extends: - .sentry - variables: - SENTRY_CMD: failure stage: Sentry + script: + - sentry-cli send-event --message "Deploy to ${ENV_NAME} failed." sentry_failure_development: extends: - .development - .sentry_deploy_failure + needs: + - job: deploy_development + optional: true + when: on_failure rules: - <<: *sentry-rule - <<: *push-only - - <<: *development-rule - when: on_failure + - *development-rules sentry_failure_staging: extends: - .staging - .sentry_deploy_failure + needs: + - job: deploy_staging + optional: true + when: on_failure rules: - <<: *sentry-rule - <<: *push-only - - <<: *staging-rule - when: on_failure + - *staging-rules sentry_failure_production: extends: - .production - .sentry_deploy_failure + needs: + - job: deploy_production + optional: true + when: on_failure rules: - <<: *sentry-rule - <<: *push-only - - <<: *production-rule - when: on_failure + - *production-rules diff --git a/{{cookiecutter.project_dirname}}/docker/local.Dockerfile b/{{cookiecutter.project_dirname}}/docker/local.Dockerfile index 66e74bc..842ff5f 100644 --- a/{{cookiecutter.project_dirname}}/docker/local.Dockerfile +++ b/{{cookiecutter.project_dirname}}/docker/local.Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 -FROM node:20-bullseye-slim +FROM node:{{ cookiecutter.node_version }}-bullseye-slim ARG DEBIAN_FRONTEND=noninteractive GROUP_ID=1000 USER_ID=1000 USER=appuser ENV LANG=C.UTF-8 LC_ALL=C.UTF-8 NEXT_TELEMETRY_DISABLED=1 NODE_ENV="development" USER=$USER WORKDIR=/app RUN apt-get update \ diff --git a/{{cookiecutter.project_dirname}}/docker/remote.Dockerfile b/{{cookiecutter.project_dirname}}/docker/remote.Dockerfile index b333c76..0645bce 100644 --- a/{{cookiecutter.project_dirname}}/docker/remote.Dockerfile +++ b/{{cookiecutter.project_dirname}}/docker/remote.Dockerfile @@ -1,56 +1,85 @@ # syntax=docker/dockerfile:1 -FROM node:20-alpine AS build -ENV PATH="$PATH:./node_modules/.bin" +ARG NODE_VERSION={{ cookiecutter.node_version }}-alpine + + +FROM node:${NODE_VERSION} AS dependencies + +LABEL company="20tab" project="{{ cookiecutter.project_slug }}" service="{{ cookiecutter.service_slug }}" stage="dependencies" + WORKDIR /app -COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ -RUN \ - if [ -f yarn.lock ]; then yarn install --frozen-lockfile; \ - elif [ -f package-lock.json ]; then npm ci; \ - elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i; \ - else echo "Lockfile not found." && exit 1; \ - fi -COPY components ./components -COPY declarations ./declarations -COPY models ./models -COPY pages ./pages -COPY public ./public -COPY styles ./styles -COPY utils ./utils -COPY tsconfig.json next.config.mjs sentry.client.config.ts sentry.server.config.ts sentry.edge.config.ts ./ -ARG SENTRY_AUTH_TOKEN \ - SENTRY_ORG \ - SENTRY_PROJECT_NAME \ - SENTRY_URL -ENV NEXT_TELEMETRY_DISABLED=1 \ - NODE_ENV="production" \ - PORT=3000 \ - SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN \ - SENTRY_ORG=$SENTRY_ORG \ - SENTRY_PROJECT_NAME=$SENTRY_PROJECT_NAME \ - SENTRY_URL=$SENTRY_URL -RUN yarn build -LABEL company="20tab" project="{{ cookiecutter.project_slug }}" service="frontend" stage="build" - -FROM node:20-alpine AS remote + +COPY package.json yarn.lock ./ + +RUN corepack enable && corepack prepare yarn@stable --activate && yarn set version stable + +RUN --mount=type=cache,target=/usr/local/share/.cache/yarn \ + yarn install --immutable + + +FROM node:${NODE_VERSION} AS builder + +LABEL company="20tab" project="{{ cookiecutter.project_slug }}" service="{{ cookiecutter.service_slug }}" stage="builder" + +ARG NEXT_PUBLIC_PROJECT_URL \ + SENTRY_DSN \ + SENTRY_ENVIRONMENT \ + SENTRY_ORG \ + SENTRY_PROJECT_NAME \ + SENTRY_TRACES_SAMPLE_RATE \ + SENTRY_URL + +ENV NEXT_PUBLIC_PROJECT_URL=$NEXT_PUBLIC_PROJECT_URL \ + NEXT_PUBLIC_ENVIRONMENT=$SENTRY_ENVIRONMENT \ + NEXT_PUBLIC_SENTRY_DSN=$SENTRY_DSN \ + NEXT_PUBLIC_SENTRY_ENVIRONMENT=$SENTRY_ENVIRONMENT \ + NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE=$SENTRY_TRACES_SAMPLE_RATE \ + NEXT_TELEMETRY_DISABLED=1 \ + NODE_ENV=production \ + PATH="$PATH:./node_modules/.bin" \ + SENTRY_ORG=$SENTRY_ORG \ + SENTRY_PROJECT_NAME=$SENTRY_PROJECT_NAME \ + SENTRY_URL=$SENTRY_URL \ + TZ="Europe/Rome" + +WORKDIR /app + +COPY --from=dependencies /app/node_modules ./node_modules + +COPY . . + +RUN --mount=type=secret,id=SENTRY_AUTH_TOKEN \ + SENTRY_AUTH_TOKEN=$(cat /run/secrets/SENTRY_AUTH_TOKEN 2>/dev/null || true) \ + yarn build + + +FROM node:${NODE_VERSION} AS remote + +LABEL company="20tab" project="{{ cookiecutter.project_slug }}" service="{{ cookiecutter.service_slug }}" stage="remote" + +ARG SENTRY_ENVIRONMENT + +ENV HOSTNAME="0.0.0.0" \ + NEXT_PUBLIC_ENVIRONMENT=$SENTRY_ENVIRONMENT \ + NEXT_PUBLIC_SENTRY_ENVIRONMENT=$SENTRY_ENVIRONMENT \ + NEXT_TELEMETRY_DISABLED=1 \ + NODE_ENV=production \ + PORT={{ cookiecutter.internal_service_port }} \ + TZ="Europe/Rome" + WORKDIR /app -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs + +RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs + +COPY --from=builder --chown=nextjs:nodejs /app/public ./public + +RUN mkdir .next && chown nextjs:nodejs .next + +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + USER nextjs -COPY ["next.config.mjs", "package.json", "server.js", "sentry.client.config.ts", "sentry.server.config.ts", "sentry.edge.config.ts", "yarn.lock", "./"] -COPY ["public/", "public/"] -COPY --from=build --chown=nextjs:nodejs /app/.next/standalone ./ -COPY --from=build --chown=nextjs:nodejs /app/.next/static ./.next/static -ARG SENTRY_AUTH_TOKEN \ - SENTRY_ORG \ - SENTRY_PROJECT_NAME \ - SENTRY_URL -ENV NEXT_TELEMETRY_DISABLED=1 \ - NODE_ENV="production" \ - PORT=3000 \ -SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN \ - SENTRY_ORG=$SENTRY_ORG \ - SENTRY_PROJECT_NAME=$SENTRY_PROJECT_NAME \ - SENTRY_URL=$SENTRY_URL -CMD yarn start -LABEL company="20tab" project="{{ cookiecutter.project_slug }}" service="frontend" stage="remote" + +EXPOSE {{ cookiecutter.internal_service_port }} + +CMD ["node", "server.js"] diff --git a/{{cookiecutter.project_dirname}}/docker/test.Dockerfile b/{{cookiecutter.project_dirname}}/docker/test.Dockerfile index 34a94e0..7407f0a 100644 --- a/{{cookiecutter.project_dirname}}/docker/test.Dockerfile +++ b/{{cookiecutter.project_dirname}}/docker/test.Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 -FROM node:20-bullseye-slim +FROM node:{{ cookiecutter.node_version }}-bullseye-slim ARG DEBIAN_FRONTEND=noninteractive GROUP_ID=1001 USER=appuser ENV APPUSER=$USER \ CYPRESS_CACHE_FOLDER="/tmp/.cache/Cypress" \ diff --git a/{{cookiecutter.project_dirname}}/minos/common.tfvars b/{{cookiecutter.project_dirname}}/minos/common.tfvars new file mode 100644 index 0000000..c73e82e --- /dev/null +++ b/{{cookiecutter.project_dirname}}/minos/common.tfvars @@ -0,0 +1,7 @@ +deployments = { + {{ cookiecutter.service_slug }} = { + port = "{{ cookiecutter.internal_service_port }}" + } +} +service_slug = "{{ cookiecutter.service_slug }}" +shared_secret_values_json = "shared-secrets.tftpl.json" diff --git a/{{cookiecutter.project_dirname}}/minos/development/shared-config.yaml b/{{cookiecutter.project_dirname}}/minos/development/shared-config.yaml new file mode 100644 index 0000000..8eeeb3e --- /dev/null +++ b/{{cookiecutter.project_dirname}}/minos/development/shared-config.yaml @@ -0,0 +1 @@ +INTERNAL_SERVICE_PORT: "{{ cookiecutter.internal_service_port }}" diff --git a/{{cookiecutter.project_dirname}}/minos/development/this.tfvars b/{{cookiecutter.project_dirname}}/minos/development/this.tfvars new file mode 100644 index 0000000..81fef41 --- /dev/null +++ b/{{cookiecutter.project_dirname}}/minos/development/this.tfvars @@ -0,0 +1,14 @@ +{% set env = cookiecutter.resources.envs|selectattr("slug", "equalto", "dev")|first %}certificates = { + primary = { + letsencrypt_email = "tech@20tab.com" + hosts = ["{{ env.host }}"] + } +} +cluster_slug = "{{ cookiecutter.project_slug }}-{{ env.cluster_slug }}" +environment = "{{ env.name }}" +namespace = "{{ cookiecutter.project_slug }}-{{ env.slug }}" +project_slug = "{{ cookiecutter.project_slug }}" +routing = { + "{{ env.host }}" = { deployment = "{{ cookiecutter.service_slug }}" } +} +shared_config_values_yaml = "{{ env.name }}/shared-config.yaml" diff --git a/{{cookiecutter.project_dirname}}/minos/production/shared-config.yaml b/{{cookiecutter.project_dirname}}/minos/production/shared-config.yaml new file mode 100644 index 0000000..8eeeb3e --- /dev/null +++ b/{{cookiecutter.project_dirname}}/minos/production/shared-config.yaml @@ -0,0 +1 @@ +INTERNAL_SERVICE_PORT: "{{ cookiecutter.internal_service_port }}" diff --git a/{{cookiecutter.project_dirname}}/minos/production/this.tfvars b/{{cookiecutter.project_dirname}}/minos/production/this.tfvars new file mode 100644 index 0000000..5603c9d --- /dev/null +++ b/{{cookiecutter.project_dirname}}/minos/production/this.tfvars @@ -0,0 +1,14 @@ +{% set env = cookiecutter.resources.envs|selectattr("slug", "equalto", "prod")|first %}certificates = { + primary = { + letsencrypt_email = "tech@20tab.com" + hosts = ["{{ env.host }}"] + } +} +cluster_slug = "{{ cookiecutter.project_slug }}-{{ env.cluster_slug }}" +environment = "{{ env.name }}" +namespace = "{{ cookiecutter.project_slug }}-{{ env.slug }}" +project_slug = "{{ cookiecutter.project_slug }}" +routing = { + "{{ env.host }}" = { deployment = "{{ cookiecutter.service_slug }}" } +} +shared_config_values_yaml = "{{ env.name }}/shared-config.yaml" diff --git a/{{cookiecutter.project_dirname}}/minos/staging/shared-config.yaml b/{{cookiecutter.project_dirname}}/minos/staging/shared-config.yaml new file mode 100644 index 0000000..8eeeb3e --- /dev/null +++ b/{{cookiecutter.project_dirname}}/minos/staging/shared-config.yaml @@ -0,0 +1 @@ +INTERNAL_SERVICE_PORT: "{{ cookiecutter.internal_service_port }}" diff --git a/{{cookiecutter.project_dirname}}/minos/staging/this.tfvars b/{{cookiecutter.project_dirname}}/minos/staging/this.tfvars new file mode 100644 index 0000000..d29f93c --- /dev/null +++ b/{{cookiecutter.project_dirname}}/minos/staging/this.tfvars @@ -0,0 +1,14 @@ +{% set env = cookiecutter.resources.envs|selectattr("slug", "equalto", "stage")|first %}certificates = { + primary = { + letsencrypt_email = "tech@20tab.com" + hosts = ["{{ env.host }}"] + } +} +cluster_slug = "{{ cookiecutter.project_slug }}-{{ env.cluster_slug }}" +environment = "{{ env.name }}" +namespace = "{{ cookiecutter.project_slug }}-{{ env.slug }}" +project_slug = "{{ cookiecutter.project_slug }}" +routing = { + "{{ env.host }}" = { deployment = "{{ cookiecutter.service_slug }}" } +} +shared_config_values_yaml = "{{ env.name }}/shared-config.yaml" diff --git a/{{cookiecutter.project_dirname}}/scripts/ci_pact.sh b/{{cookiecutter.project_dirname}}/scripts/ci_pact.sh index 958c5e0..ded06f1 100755 --- a/{{cookiecutter.project_dirname}}/scripts/ci_pact.sh +++ b/{{cookiecutter.project_dirname}}/scripts/ci_pact.sh @@ -1,13 +1,20 @@ #!/usr/bin/env sh -set -e +set -eu if [ "${VAULT_ADDR}" != "" ]; then apk update && apk add curl jq - vault_token=$(curl --silent --request POST --data "role=pact" --data "jwt=${VAULT_ID_TOKEN}" "${VAULT_ADDR%/}"/v1/auth/gitlab-jwt/login | jq -r .auth.client_token) + vault_token=$(curl --silent --request POST \ + --data "role=pact" \ + --data "jwt=${VAULT_ID_TOKEN}" \ + "${VAULT_ADDR%/}"/v1/auth/gitlab-jwt/login | \ + jq -r .auth.client_token) - pact_secrets=$(curl --silent --header "X-Vault-Token: ${vault_token}" "${VAULT_ADDR%/}"/v1/"${PROJECT_SLUG}"/pact | jq -r .data) + pact_secrets=$(curl --silent \ + --header "X-Vault-Token: ${vault_token}" \ + "${VAULT_ADDR%/}"/v1/"${PROJECT_SLUG}"/pact | \ + jq -r .data) PACT_BROKER_BASE_URL=$(echo "${pact_secrets}" | jq -r .pact_broker_base_url) PACT_BROKER_PASSWORD=$(echo "${pact_secrets}" | jq -r .pact_broker_password) @@ -17,5 +24,3 @@ if [ "${VAULT_ADDR}" != "" ]; then export PACT_BROKER_PASSWORD export PACT_BROKER_USERNAME fi - -docker-entrypoint.sh pact-broker "${@}" diff --git a/{{cookiecutter.project_dirname}}/scripts/ci_sentry.sh b/{{cookiecutter.project_dirname}}/scripts/ci_sentry.sh index 217b5ac..dc42d59 100755 --- a/{{cookiecutter.project_dirname}}/scripts/ci_sentry.sh +++ b/{{cookiecutter.project_dirname}}/scripts/ci_sentry.sh @@ -9,24 +9,24 @@ git config --global --add safe.directory "${PROJECT_DIR}" if [ "${VAULT_ADDR}" != "" ]; then apk add curl jq - vault_token=$(curl --silent --request POST --data "role=${VAULT_ROLE}" --data "jwt=${VAULT_ID_TOKEN}" "${VAULT_ADDR%/}"/v1/auth/gitlab-jwt/login | jq -r .auth.client_token) + vault_token=$(curl --silent --request POST \ + --data "role=${VAULT_ROLE}" \ + --data "jwt=${VAULT_ID_TOKEN}" \ + "${VAULT_ADDR%/}"/v1/auth/gitlab-jwt/login | \ + jq -r .auth.client_token) + + vault_base_secrets_addr="${VAULT_ADDR%/}/v1/${PROJECT_SLUG}/envs/${ENV_NAME}" + + SENTRY_AUTH_TOKEN=$(curl --silent \ + --header "X-Vault-Token: ${vault_token}" \ + "${vault_base_secrets_addr}/sentry" | \ + jq -r .data.sentry_auth_token) + + SENTRY_DSN=$(curl --silent \ + --header "X-Vault-Token: ${vault_token}" \ + "${vault_base_secrets_addr}/${SERVICE_SLUG}/sentry" | \ + jq -r .data.sentry_dsn) - SENTRY_AUTH_TOKEN=$(curl --silent --header "X-Vault-Token: ${vault_token}" "${VAULT_ADDR%/}"/v1/"${PROJECT_SLUG}"/envs/"${ENV_NAME}"/sentry | jq -r .data.sentry_auth_token) - SENTRY_DSN=$(curl --silent --header "X-Vault-Token: ${vault_token}" "${VAULT_ADDR%/}"/v1/"${PROJECT_SLUG}"/envs/"${ENV_NAME}"/"${SERVICE_SLUG}"/sentry | jq -r .data.sentry_dsn) export SENTRY_AUTH_TOKEN export SENTRY_DSN fi - -case "${1}" in - "release") - sentry-cli releases new "${VERSION_REF}" -p "${SENTRY_PROJECT_NAME}" --log-level=debug; - sentry-cli releases set-commits "${VERSION_REF}" --auto --ignore-missing; - sentry-cli releases finalize "${VERSION_REF}"; - ;; - "success") - sentry-cli releases deploys "${VERSION_REF}" new -e "${CI_ENVIRONMENT_NAME}" -t $((RELEASE_END-RELEASE_START)); - ;; - "failure") - sentry-cli send-event -m "Deploy to ${CI_ENVIRONMENT_NAME} failed."; - ;; -esac diff --git a/{{cookiecutter.project_dirname}}/scripts/deploy/gitlab.sh b/{{cookiecutter.project_dirname}}/scripts/deploy/gitlab.sh deleted file mode 100755 index 69a45af..0000000 --- a/{{cookiecutter.project_dirname}}/scripts/deploy/gitlab.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env sh - -set -e - -# If TF_USERNAME is unset then default to GITLAB_USER_LOGIN -TF_USERNAME="${TF_USERNAME:-${GITLAB_USER_LOGIN}}" -# If TF_PASSWORD is unset then default to gitlab-ci-token/CI_JOB_TOKEN -if [ -z "${TF_PASSWORD}" ]; then -TF_USERNAME="gitlab-ci-token" -TF_PASSWORD="${CI_JOB_TOKEN}" -fi -# If TF_ADDRESS is unset but TF_STATE_NAME is provided, then default to GitLab backend in current project -if [ -n "${TF_STATE_NAME}" ]; then -TF_ADDRESS="${TF_ADDRESS:-${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${TF_STATE_NAME}}" -fi -# Set variables for the HTTP backend to default to TF_* values -export TF_HTTP_ADDRESS="${TF_HTTP_ADDRESS:-${TF_ADDRESS}}" -export TF_HTTP_LOCK_ADDRESS="${TF_HTTP_LOCK_ADDRESS:-${TF_ADDRESS}/lock}" -export TF_HTTP_LOCK_METHOD="${TF_HTTP_LOCK_METHOD:-POST}" -export TF_HTTP_UNLOCK_ADDRESS="${TF_HTTP_UNLOCK_ADDRESS:-${TF_ADDRESS}/lock}" -export TF_HTTP_UNLOCK_METHOD="${TF_HTTP_UNLOCK_METHOD:-DELETE}" -export TF_HTTP_USERNAME="${TF_HTTP_USERNAME:-${TF_USERNAME}}" -export TF_HTTP_PASSWORD="${TF_HTTP_PASSWORD:-${TF_PASSWORD}}" -export TF_HTTP_RETRY_WAIT_MIN="${TF_HTTP_RETRY_WAIT_MIN:-5}" -# Expose Gitlab specific variables to terraform since no -tf-var is available -# Usable in the .tf file as variable "CI_JOB_ID" { type = string } etc -export TF_VAR_CI_JOB_ID="${TF_VAR_CI_JOB_ID:-${CI_JOB_ID}}" -export TF_VAR_CI_COMMIT_SHA="${TF_VAR_CI_COMMIT_SHA:-${CI_COMMIT_SHA}}" -export TF_VAR_CI_JOB_STAGE="${TF_VAR_CI_JOB_STAGE:-${CI_JOB_STAGE}}" -export TF_VAR_CI_PROJECT_ID="${TF_VAR_CI_PROJECT_ID:-${CI_PROJECT_ID}}" -export TF_VAR_CI_PROJECT_NAME="${TF_VAR_CI_PROJECT_NAME:-${CI_PROJECT_NAME}}" -export TF_VAR_CI_PROJECT_NAMESPACE="${TF_VAR_CI_PROJECT_NAMESPACE:-${CI_PROJECT_NAMESPACE}}" -export TF_VAR_CI_PROJECT_PATH="${TF_VAR_CI_PROJECT_PATH:-${CI_PROJECT_PATH}}" -export TF_VAR_CI_PROJECT_URL="${TF_VAR_CI_PROJECT_URL:-${CI_PROJECT_URL}}" diff --git a/{{cookiecutter.project_dirname}}/scripts/deploy/init.sh b/{{cookiecutter.project_dirname}}/scripts/deploy/init.sh deleted file mode 100755 index c5902c6..0000000 --- a/{{cookiecutter.project_dirname}}/scripts/deploy/init.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env sh - -set -e - -export TF_VAR_env_slug="${ENV_SLUG}" -export TF_VAR_project_slug="${PROJECT_SLUG}" -export TF_VAR_stack_slug="${STACK_SLUG}" - -terraform_cli_args="-var-file=${TERRAFORM_VARS_DIR%/}/.tfvars" - -if [ "${TERRAFORM_EXTRA_VAR_FILE}" != "" ]; then - extra_var_file="${TERRAFORM_VARS_DIR%/}/${TERRAFORM_EXTRA_VAR_FILE}" - touch "${extra_var_file}" - terraform_cli_args="${terraform_cli_args} -var-file=${extra_var_file}" -fi - -if [ "${VAULT_ADDR}" != "" ]; then - . "${PROJECT_DIR}"/scripts/deploy/vault.sh - terraform_cli_args="${terraform_cli_args} -var-file=${TERRAFORM_VARS_DIR%/}/vault-secrets.tfvars.json" -fi - -export TF_CLI_ARGS_destroy="${terraform_cli_args}" -export TF_CLI_ARGS_plan="${terraform_cli_args}" - -case "${TERRAFORM_BACKEND}" in - "gitlab") - . "${PROJECT_DIR}"/scripts/deploy/gitlab.sh - ;; - "terraform-cloud") - . "${PROJECT_DIR}"/scripts/deploy/terraform-cloud.sh - ;; -esac diff --git a/{{cookiecutter.project_dirname}}/scripts/deploy/terraform-cloud.sh b/{{cookiecutter.project_dirname}}/scripts/deploy/terraform-cloud.sh deleted file mode 100755 index b01a928..0000000 --- a/{{cookiecutter.project_dirname}}/scripts/deploy/terraform-cloud.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env sh - -set -e - -export TF_CLI_CONFIG_FILE="${TF_ROOT}/cloud.tfc" -cat << EOF > "${TF_CLI_CONFIG_FILE}" -{ - "credentials": { - "app.terraform.io": { - "token": "${TFC_TOKEN}" - } - } -} -EOF diff --git a/{{cookiecutter.project_dirname}}/scripts/deploy/terraform.sh b/{{cookiecutter.project_dirname}}/scripts/deploy/terraform.sh deleted file mode 100755 index e8dab53..0000000 --- a/{{cookiecutter.project_dirname}}/scripts/deploy/terraform.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env sh - -set -e - -if [ "${DEBUG_OUTPUT}" = "true" ]; then - set -ex -fi - -plan_cache="plan.cache" -plan_json="plan.json" - -JQ_PLAN=' - ( - [.resource_changes[]?.change.actions?] | flatten - ) | { - "create":(map(select(.=="create")) | length), - "update":(map(select(.=="update")) | length), - "delete":(map(select(.=="delete")) | length) - } -' - -# Use terraform automation mode (will remove some verbose unneeded messages) -export TF_IN_AUTOMATION=true - -init() { - cd "${TF_ROOT}" - if [ "${TERRAFORM_BACKEND}" = "terraform-cloud" ]; then - terraform init "${@}" -input=false - else - terraform init "${@}" -input=false -reconfigure - fi -} - -case "${1}" in - "apply") - init - terraform "${@}" -input=false "${plan_cache}" - ;; - "destroy") - init - terraform "${@}" -auto-approve - ;; - "fmt") - terraform "${@}" -check -diff -recursive - ;; - "init") - # shift argument list „one to the left“ to not call 'terraform init init' - shift - init "${@}" - ;; - "plan") - init - terraform "${@}" -input=false -out="${plan_cache}" - ;; - "plan-json") - init - terraform plan -input=false -out="${plan_cache}" - terraform show -json "${plan_cache}" | \ - jq -r "${JQ_PLAN}" \ - > "${plan_json}" - ;; - "validate") - init -backend=false - terraform "${@}" - ;; - *) - terraform "${@}" - ;; -esac diff --git a/{{cookiecutter.project_dirname}}/scripts/deploy/vault.sh b/{{cookiecutter.project_dirname}}/scripts/deploy/vault.sh deleted file mode 100755 index 402802d..0000000 --- a/{{cookiecutter.project_dirname}}/scripts/deploy/vault.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env sh - -set -e - -vault_token=$(curl --silent --request POST --data "role=${VAULT_ROLE}" --data "jwt=${VAULT_ID_TOKEN}" "${VAULT_ADDR%/}"/v1/auth/gitlab-jwt/login | jq -r .auth.client_token) - -secrets_data="{}" - -for secret_path in ${VAULT_SECRETS} -do - secret_data=$(curl --silent --header "X-Vault-Token: ${vault_token}" "${VAULT_ADDR%/}"/v1/"${PROJECT_SLUG}"/"${VAULT_SECRETS_PREFIX}"/"${secret_path}" | jq -r '.data // {}') || secret_data="{}" - secrets_data=$(echo "${secrets_data}" | jq --argjson new_data "${secret_data}" '. * $new_data') -done - -echo "${secrets_data}" > "${TERRAFORM_VARS_DIR%/}"/vault-secrets.tfvars.json - -if [ "${TERRAFORM_BACKEND}" = "terraform-cloud" ]; then - TFC_TOKEN=$(curl --silent --header "X-Vault-Token: ${vault_token}" "${VAULT_ADDR%/}"/v1/"${PROJECT_SLUG}"-tfc/creds/default | jq -r .data.token) - export TFC_TOKEN -fi diff --git a/{{cookiecutter.project_dirname}}/terraform/digitalocean-k8s/main.tf b/{{cookiecutter.project_dirname}}/terraform/digitalocean-k8s/main.tf deleted file mode 100644 index 831e945..0000000 --- a/{{cookiecutter.project_dirname}}/terraform/digitalocean-k8s/main.tf +++ /dev/null @@ -1,68 +0,0 @@ -locals { - environment_slug = { development = "dev", staging = "stage", production = "prod" }[lower(var.environment)] - - namespace = "${var.project_slug}-${local.environment_slug}" - - cluster_prefix = var.stack_slug == "main" ? var.project_slug : "${var.project_slug}-${var.stack_slug}" -} - -terraform { - required_providers { - digitalocean = { - source = "digitalocean/digitalocean" - version = "~> 2.36" - } - kubernetes = { - source = "hashicorp/kubernetes" - version = "~> 2.27" - } - } -} - -/* Providers */ - -provider "digitalocean" { - token = var.digitalocean_token -} - -provider "kubernetes" { - host = data.digitalocean_kubernetes_cluster.main.endpoint - token = data.digitalocean_kubernetes_cluster.main.kube_config[0].token - cluster_ca_certificate = base64decode( - data.digitalocean_kubernetes_cluster.main.kube_config[0].cluster_ca_certificate - ) -} - -/* Data Sources */ - -data "digitalocean_kubernetes_cluster" "main" { - name = "${local.cluster_prefix}-k8s-cluster" -} - -/* Deployment */ - -module "deployment" { - source = "../modules/kubernetes/deployment" - - environment = var.environment - - namespace = local.namespace - - project_slug = var.project_slug - project_url = var.project_url - - service_container_image = var.service_container_image - service_container_port = var.service_container_port - service_limits_cpu = var.service_limits_cpu - service_limits_memory = var.service_limits_memory - service_replicas = var.service_replicas - service_requests_cpu = var.service_requests_cpu - service_requests_memory = var.service_requests_memory - service_slug = var.service_slug - - internal_backend_url = var.internal_backend_url - sentry_dsn = var.sentry_dsn - - extra_config_values = var.extra_config_values - extra_secret_values = var.extra_secret_values -} diff --git a/{{cookiecutter.project_dirname}}/terraform/digitalocean-k8s/variables.tf b/{{cookiecutter.project_dirname}}/terraform/digitalocean-k8s/variables.tf deleted file mode 100644 index 9da82f6..0000000 --- a/{{cookiecutter.project_dirname}}/terraform/digitalocean-k8s/variables.tf +++ /dev/null @@ -1,97 +0,0 @@ -variable "digitalocean_token" { - description = "The Digital Ocean access token." - type = string - sensitive = true -} - -variable "internal_backend_url" { - description = "The internal backend url." - type = string - default = "" -} - -variable "environment" { - type = string - description = "The name of the deploy environment, e.g. \"Production\"." -} - -variable "extra_config_values" { - type = map(string) - description = "Additional config map environment variables." - default = {} -} - -variable "extra_secret_values" { - type = map(string) - description = "Additional secret environment variables." - default = {} - sensitive = true -} - -variable "project_slug" { - description = "The project slug." - type = string -} - -variable "project_url" { - description = "The project url." - type = string -} - -variable "sentry_dsn" { - description = "The Sentry project DSN." - type = string - default = "" - sensitive = true -} - -variable "service_container_image" { - description = "The service container image." - type = string -} - -variable "service_container_port" { - description = "The service container port." - type = string - default = "{{ cookiecutter.internal_service_port }}" -} - -variable "service_limits_cpu" { - description = "The service limits cpu value." - type = string - default = null -} - -variable "service_limits_memory" { - description = "The service limits memory value." - type = string - default = null -} - -variable "service_replicas" { - description = "The desired numbers of replicas to deploy." - type = number - default = 1 -} - -variable "service_requests_cpu" { - description = "The service requests cpu value." - type = string - default = null -} - -variable "service_requests_memory" { - description = "The service requests memory value." - type = string - default = null -} - -variable "service_slug" { - description = "The service slug." - type = string -} - -variable "stack_slug" { - description = "The slug of the stack where the service is deployed." - type = string -} diff --git "a/{{cookiecutter.project_dirname}}/terraform/digitalocean-k8s/{% if cookiecutter.terraform_backend == \"gitlab\" %}backend.tf{% endif %}" "b/{{cookiecutter.project_dirname}}/terraform/digitalocean-k8s/{% if cookiecutter.terraform_backend == \"gitlab\" %}backend.tf{% endif %}" deleted file mode 100644 index 4ca44e9..0000000 --- "a/{{cookiecutter.project_dirname}}/terraform/digitalocean-k8s/{% if cookiecutter.terraform_backend == \"gitlab\" %}backend.tf{% endif %}" +++ /dev/null @@ -1,4 +0,0 @@ -terraform { - backend "http" { - } -} diff --git "a/{{cookiecutter.project_dirname}}/terraform/digitalocean-k8s/{% if cookiecutter.terraform_backend == \"terraform-cloud\" %}cloud.tf{% endif %}" "b/{{cookiecutter.project_dirname}}/terraform/digitalocean-k8s/{% if cookiecutter.terraform_backend == \"terraform-cloud\" %}cloud.tf{% endif %}" deleted file mode 100644 index 3849a36..0000000 --- "a/{{cookiecutter.project_dirname}}/terraform/digitalocean-k8s/{% if cookiecutter.terraform_backend == \"terraform-cloud\" %}cloud.tf{% endif %}" +++ /dev/null @@ -1,9 +0,0 @@ -terraform { - cloud { - organization = "{{ cookiecutter.terraform_cloud_organization }}" - - workspaces { - tags = ["project:{{ cookiecutter.project_slug }}"] - } - } -} diff --git a/{{cookiecutter.project_dirname}}/terraform/modules/kubernetes/deployment/main.tf b/{{cookiecutter.project_dirname}}/terraform/modules/kubernetes/deployment/main.tf deleted file mode 100644 index 0927e04..0000000 --- a/{{cookiecutter.project_dirname}}/terraform/modules/kubernetes/deployment/main.tf +++ /dev/null @@ -1,127 +0,0 @@ -locals { - service_labels = { - component = var.service_slug - environment = var.environment - project = var.project_slug - terraform = "true" - } -} - -terraform { - required_providers { - kubernetes = { - source = "hashicorp/kubernetes" - version = "~> 2.27" - } - } -} - -/* Secrets */ - -resource "kubernetes_secret_v1" "main" { - - metadata { - name = "${var.service_slug}-env-vars" - namespace = var.namespace - } - - data = { for k, v in merge( - var.extra_secret_values, - { - NEXT_PUBLIC_SENTRY_DSN = var.sentry_dsn - } - ) : k => v if v != "" } -} - -/* Config Map */ - -resource "kubernetes_config_map_v1" "main" { - metadata { - name = "${var.service_slug}-env-vars" - namespace = var.namespace - } - - data = { for k, v in merge( - var.extra_config_values, - { - INTERNAL_BACKEND_URL = var.internal_backend_url - PORT = var.service_container_port - NEXT_PUBLIC_PROJECT_URL = var.project_url - REACT_ENVIRONMENT = var.environment - } - ) : k => v if v != "" } -} - -/* Deployment */ - -resource "kubernetes_deployment_v1" "main" { - metadata { - name = var.service_slug - namespace = var.namespace - annotations = { - "reloader.stakater.com/auto" = "true" - } - } - spec { - replicas = var.service_replicas - selector { - match_labels = local.service_labels - } - template { - metadata { - labels = local.service_labels - } - spec { - image_pull_secrets { - name = "regcred" - } - container { - image = var.service_container_image - name = var.service_slug - resources { - limits = { - cpu = var.service_limits_cpu - memory = var.service_limits_memory - } - requests = { - cpu = var.service_requests_cpu - memory = var.service_requests_memory - } - } - port { - container_port = var.service_container_port - } - env_from { - config_map_ref { - name = kubernetes_config_map_v1.main.metadata[0].name - } - } - env_from { - secret_ref { - name = kubernetes_secret_v1.main.metadata[0].name - } - } - } - } - } - } -} - -/* Cluster IP Service */ - -resource "kubernetes_service_v1" "cluster_ip" { - metadata { - name = var.service_slug - namespace = var.namespace - } - spec { - type = "ClusterIP" - selector = { - component = var.service_slug - } - port { - port = var.service_container_port - target_port = var.service_container_port - } - } -} diff --git a/{{cookiecutter.project_dirname}}/terraform/modules/kubernetes/deployment/variables.tf b/{{cookiecutter.project_dirname}}/terraform/modules/kubernetes/deployment/variables.tf deleted file mode 100644 index bec3689..0000000 --- a/{{cookiecutter.project_dirname}}/terraform/modules/kubernetes/deployment/variables.tf +++ /dev/null @@ -1,91 +0,0 @@ -variable "environment" { - type = string - description = "The deploy environment name, e.g. \"production\"." -} - -variable "extra_config_values" { - type = map(string) - description = "Additional config map environment variables." - default = {} -} - -variable "extra_secret_values" { - type = map(string) - description = "Additional secret environment variables." - default = {} - sensitive = true -} - -variable "internal_backend_url" { - description = "The internal backend url." - type = string - default = "" -} - -variable "namespace" { - description = "The Kubernetes namespace." - type = string -} - -variable "project_slug" { - description = "The project slug." - type = string -} - -variable "project_url" { - description = "The project url." - type = string -} - -variable "sentry_dsn" { - description = "The Sentry project DSN." - type = string - default = "" - sensitive = true -} - -variable "service_container_image" { - description = "The service container image." - type = string -} - -variable "service_container_port" { - description = "The service container port." - type = string - default = "{{ cookiecutter.internal_service_port }}" -} - -variable "service_limits_cpu" { - description = "The service limits cpu value." - type = string - default = null -} - -variable "service_limits_memory" { - description = "The service limits memory value." - type = string - default = null -} - -variable "service_replicas" { - description = "The desired numbers of replicas to deploy." - type = number - default = 1 -} - -variable "service_requests_cpu" { - description = "The service requests cpu value." - type = string - default = null -} - -variable "service_requests_memory" { - description = "The service requests memory value." - type = string - default = null -} - -variable "service_slug" { - description = "The service slug." - type = string -} diff --git a/{{cookiecutter.project_dirname}}/terraform/other-k8s/main.tf b/{{cookiecutter.project_dirname}}/terraform/other-k8s/main.tf deleted file mode 100644 index face8da..0000000 --- a/{{cookiecutter.project_dirname}}/terraform/other-k8s/main.tf +++ /dev/null @@ -1,52 +0,0 @@ -locals { - environment_slug = { development = "dev", staging = "stage", production = "prod" }[lower(var.environment)] - - namespace = "${var.project_slug}-${local.environment_slug}" - - cluster_prefix = var.stack_slug == "main" ? var.project_slug : "${var.project_slug}-${var.stack_slug}" -} - -terraform { - required_providers { - kubernetes = { - source = "hashicorp/kubernetes" - version = "~> 2.27" - } - } -} - -/* Providers */ - -provider "kubernetes" { - host = var.kubernetes_host - token = var.kubernetes_token - cluster_ca_certificate = base64decode(var.kubernetes_cluster_ca_certificate) -} - -/* Deployment */ - -module "deployment" { - source = "../modules/kubernetes/deployment" - - environment = var.environment - - namespace = local.namespace - - project_slug = var.project_slug - project_url = var.project_url - - service_container_image = var.service_container_image - service_container_port = var.service_container_port - service_limits_cpu = var.service_limits_cpu - service_limits_memory = var.service_limits_memory - service_replicas = var.service_replicas - service_requests_cpu = var.service_requests_cpu - service_requests_memory = var.service_requests_memory - service_slug = var.service_slug - - internal_backend_url = var.internal_backend_url - sentry_dsn = var.sentry_dsn - - extra_config_values = var.extra_config_values - extra_secret_values = var.extra_secret_values -} diff --git a/{{cookiecutter.project_dirname}}/terraform/other-k8s/variables.tf b/{{cookiecutter.project_dirname}}/terraform/other-k8s/variables.tf deleted file mode 100644 index c877e33..0000000 --- a/{{cookiecutter.project_dirname}}/terraform/other-k8s/variables.tf +++ /dev/null @@ -1,109 +0,0 @@ - -variable "environment" { - type = string - description = "The name of the deploy environment, e.g. \"Production\"." -} - -variable "extra_config_values" { - type = map(string) - description = "Additional config map environment variables." - default = {} -} - -variable "extra_secret_values" { - type = map(string) - description = "Additional secret environment variables." - default = {} - sensitive = true -} - -variable "internal_backend_url" { - description = "The internal backend url." - type = string - default = "" -} - -variable "kubernetes_cluster_ca_certificate" { - description = "The base64 encoded Kubernetes CA certificate." - type = string - sensitive = true -} - -variable "kubernetes_host" { - description = "The Kubernetes host." - type = string -} - -variable "kubernetes_token" { - description = "A Kubernetes admin token." - type = string - sensitive = true -} - -variable "project_slug" { - description = "The project slug." - type = string -} - -variable "project_url" { - description = "The project url." - type = string -} - -variable "sentry_dsn" { - description = "The Sentry project DSN." - type = string - default = "" - sensitive = true -} - -variable "service_container_image" { - description = "The service container image." - type = string -} - -variable "service_container_port" { - description = "The service container port." - type = string - default = "{{ cookiecutter.internal_service_port }}" -} - -variable "service_limits_cpu" { - description = "The service limits cpu value." - type = string - default = null -} - -variable "service_limits_memory" { - description = "The service limits memory value." - type = string - default = null -} - -variable "service_replicas" { - description = "The desired numbers of replicas to deploy." - type = number - default = 1 -} - -variable "service_requests_cpu" { - description = "The service requests cpu value." - type = string - default = null -} - -variable "service_requests_memory" { - description = "The service requests memory value." - type = string - default = null -} - -variable "service_slug" { - description = "The service slug." - type = string -} - -variable "stack_slug" { - description = "The slug of the stack where the service is deployed." - type = string -} diff --git "a/{{cookiecutter.project_dirname}}/terraform/other-k8s/{% if cookiecutter.terraform_backend == \"gitlab\" %}backend.tf{% endif %}" "b/{{cookiecutter.project_dirname}}/terraform/other-k8s/{% if cookiecutter.terraform_backend == \"gitlab\" %}backend.tf{% endif %}" deleted file mode 100644 index 4ca44e9..0000000 --- "a/{{cookiecutter.project_dirname}}/terraform/other-k8s/{% if cookiecutter.terraform_backend == \"gitlab\" %}backend.tf{% endif %}" +++ /dev/null @@ -1,4 +0,0 @@ -terraform { - backend "http" { - } -} diff --git "a/{{cookiecutter.project_dirname}}/terraform/other-k8s/{% if cookiecutter.terraform_backend == \"terraform-cloud\" %}cloud.tf{% endif %}" "b/{{cookiecutter.project_dirname}}/terraform/other-k8s/{% if cookiecutter.terraform_backend == \"terraform-cloud\" %}cloud.tf{% endif %}" deleted file mode 100644 index 3849a36..0000000 --- "a/{{cookiecutter.project_dirname}}/terraform/other-k8s/{% if cookiecutter.terraform_backend == \"terraform-cloud\" %}cloud.tf{% endif %}" +++ /dev/null @@ -1,9 +0,0 @@ -terraform { - cloud { - organization = "{{ cookiecutter.terraform_cloud_organization }}" - - workspaces { - tags = ["project:{{ cookiecutter.project_slug }}"] - } - } -} diff --git a/{{cookiecutter.project_dirname}}/terraform/vars/.tfvars b/{{cookiecutter.project_dirname}}/terraform/vars/.tfvars deleted file mode 100644 index afa9d4a..0000000 --- a/{{cookiecutter.project_dirname}}/terraform/vars/.tfvars +++ /dev/null @@ -1,7 +0,0 @@ -{% if "environment" in cookiecutter.tfvars %}{% for item in cookiecutter.tfvars.environment|sort %}{{ item }} -{% endfor %}{% endif %}# service_container_port="{{ cookiecutter.internal_service_port }}" -# service_limits_cpu="225m" -# service_limits_memory="256Mi" -# service_replicas=1 -# service_requests_cpu="25m" -# service_requests_memory="115Mi" diff --git "a/{{cookiecutter.project_dirname}}/terraform/vars/{% if \"environment_dev\" in cookiecutter.tfvars %}dev.tfvars{% endif %}" "b/{{cookiecutter.project_dirname}}/terraform/vars/{% if \"environment_dev\" in cookiecutter.tfvars %}dev.tfvars{% endif %}" deleted file mode 100644 index e2140f3..0000000 --- "a/{{cookiecutter.project_dirname}}/terraform/vars/{% if \"environment_dev\" in cookiecutter.tfvars %}dev.tfvars{% endif %}" +++ /dev/null @@ -1,2 +0,0 @@ -{% for item in cookiecutter.tfvars.environment_dev|sort %}{{ item }} -{% endfor %} diff --git "a/{{cookiecutter.project_dirname}}/terraform/vars/{% if \"environment_prod\" in cookiecutter.tfvars %}prod.tfvars{% endif %}" "b/{{cookiecutter.project_dirname}}/terraform/vars/{% if \"environment_prod\" in cookiecutter.tfvars %}prod.tfvars{% endif %}" deleted file mode 100644 index 6700bcd..0000000 --- "a/{{cookiecutter.project_dirname}}/terraform/vars/{% if \"environment_prod\" in cookiecutter.tfvars %}prod.tfvars{% endif %}" +++ /dev/null @@ -1,2 +0,0 @@ -{% for item in cookiecutter.tfvars.environment_prod|sort %}{{ item }} -{% endfor %} diff --git "a/{{cookiecutter.project_dirname}}/terraform/vars/{% if \"environment_stage\" in cookiecutter.tfvars %}stage.tfvars{% endif %}" "b/{{cookiecutter.project_dirname}}/terraform/vars/{% if \"environment_stage\" in cookiecutter.tfvars %}stage.tfvars{% endif %}" deleted file mode 100644 index 11fc680..0000000 --- "a/{{cookiecutter.project_dirname}}/terraform/vars/{% if \"environment_stage\" in cookiecutter.tfvars %}stage.tfvars{% endif %}" +++ /dev/null @@ -1,2 +0,0 @@ -{% for item in cookiecutter.tfvars.environment_stage|sort %}{{ item }} -{% endfor %}