From 3d497d1748f7fe263cff17cd97d01afb116941fd Mon Sep 17 00:00:00 2001 From: Leo Iorio Date: Mon, 30 Mar 2026 11:42:56 -0300 Subject: [PATCH 01/15] checking new variable from ssm before big commit --- .github/workflows/CD.yml | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index c59b314..5f7ffdc 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -57,6 +57,43 @@ jobs: fi echo "STACK_NAME=ReservationStackScheduleANDCourts${{env.STAGE}}" >> $GITHUB_ENV + - name: Fetch config from SSM + run: | + STAGE="${{ github.ref_name }}" + + USER_API_BASE=$(aws ssm get-parameter \ + --name "/reservationmssuser/$STAGE/api/url" \ + --query "Parameter.Value" --output text) || true + + if [ -n "$USER_API_BASE" ]; then + echo "::add-mask::$USER_API_BASE" + fi + + echo "USER_API_BASE=$USER_API_BASE" >> $GITHUB_ENV + + - name: Validate variables from ssm + shell: /usr/bin/bash -e {0} + run: | + missing=() + + check_var() { + local key=$1 + local value=$2 + if [ -z "$value" ]; then + missing+=("$key") + fi + } + + check_var "USER_API_BASE" "$USER_API_BASE" + + if [ ${#missing[@]} -gt 0 ]; then + echo "❌ Missing environment variables:" + printf ' - %s\n' "${missing[@]}" + exit 1 + fi + + echo "✅ All environment variables from ssm are set" + - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: From ce79a235155f7c731f319d4af22a742329690cfe Mon Sep 17 00:00:00 2001 From: Leo Iorio Date: Mon, 30 Mar 2026 11:45:56 -0300 Subject: [PATCH 02/15] refactored infra, updated packages, added ssm logic to CD --- .github/workflows/CD.yml | 2 +- iac/components/apigw_construct.py | 82 ++++++++++ iac/components/dynamo_construct.py | 43 +++++ iac/components/lambda_construct.py | 251 +++++++++++++++++++++++++++++ iac/components/s3_construct.py | 43 +++++ iac/components/ssm_construct.py | 55 +++++++ iac/requirements-infra.txt | 1 + iac/stack/__init__.py | 0 iac/stack/iac_stack.py | 116 +++++++++++++ requirements-app.txt | 10 ++ 10 files changed, 602 insertions(+), 1 deletion(-) create mode 100644 iac/components/apigw_construct.py create mode 100644 iac/components/dynamo_construct.py create mode 100644 iac/components/lambda_construct.py create mode 100644 iac/components/s3_construct.py create mode 100644 iac/components/ssm_construct.py create mode 100644 iac/requirements-infra.txt create mode 100644 iac/stack/__init__.py create mode 100644 iac/stack/iac_stack.py create mode 100644 requirements-app.txt diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index 5f7ffdc..cf40783 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -122,4 +122,4 @@ jobs: FROM_EMAIL: ${{ vars.FROM_EMAIL }} HIDDEN_COPY: ${{ vars.HIDDEN_COPY }} REPLY_TO_EMAIL: ${{ vars.REPLY_TO_EMAIL }} - USER_API_URL: ${{ secrets.USER_API_URL }} + USER_API_URL: ${{ env.USER_API_BASE }} diff --git a/iac/components/apigw_construct.py b/iac/components/apigw_construct.py new file mode 100644 index 0000000..3e4cc89 --- /dev/null +++ b/iac/components/apigw_construct.py @@ -0,0 +1,82 @@ +from aws_cdk import Resource, aws_apigateway as apigateway +from constructs import Construct +from aws_cdk.aws_apigateway import RestApi, Cors, CorsOptions, GatewayResponse, ResponseType + + +class ApigwConstruct(Construct): + rest_api: RestApi + api_gateway_resource: Resource + + def __init__( + self, + scope: Construct, + construct_id: str, + stage: str, + **kwargs + ): + + stage = stage.capitalize() + + super().__init__(scope, construct_id, **kwargs) + + cors_options = CorsOptions( + allow_origins = + [ + "https://reservation.maua.br", + "https://reservation.devmaua.com" + ] + if stage == 'Prod' + else + [ + "https://reservation.hml.devmaua.com", + "https://reservation.dev.devmaua.com", + "https://localhost:3000", + "http://localhost:3000" + ], + allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], + allow_headers=Cors.DEFAULT_HEADERS + ) + + self.rest_api = RestApi( + self, f"ReservationApi_RestApi_{stage}", + rest_api_name=f"ReservationApi_RestApi_{stage}", + description="This is the Maua Reservation RestApi", + deploy_options=apigateway.StageOptions( + stage_name=stage.lower(), + logging_level=apigateway.MethodLoggingLevel.INFO, + data_trace_enabled=False, + metrics_enabled=True, + ), + default_cors_preflight_options=cors_options + ) + + GatewayResponse( + self, + "AuthorizerDenyResponse", + rest_api=self.rest_api, + type=ResponseType.ACCESS_DENIED, + response_headers={ + "Access-Control-Allow-Origin": "'*'", + "Access-Control-Allow-Headers": "'*'", + "Access-Control-Allow-Methods": "'*'", + }, + status_code="403" + ) + + GatewayResponse( + self, + "AuthorizerUnauthorizedResponse", + rest_api=self.rest_api, + type=ResponseType.UNAUTHORIZED, + response_headers={ + "Access-Control-Allow-Origin": "'*'", + "Access-Control-Allow-Headers": "'*'", + "Access-Control-Allow-Methods": "'*'", + }, + status_code="401" + ) + + self.api_gateway_resource = self.rest_api.root.add_resource( + "reservation-api", + default_cors_preflight_options=cors_options + ) \ No newline at end of file diff --git a/iac/components/dynamo_construct.py b/iac/components/dynamo_construct.py new file mode 100644 index 0000000..a753ae1 --- /dev/null +++ b/iac/components/dynamo_construct.py @@ -0,0 +1,43 @@ +from aws_cdk import ( + aws_dynamodb as dynamodb, RemovalPolicy, +) +from constructs import Construct + +class DynamoConstruct(Construct): + table: dynamodb.Table + + def __init__( + self, + scope: Construct, + construct_id: str, + stage: str, + **kwargs + ) -> None: + super().__init__(scope, construct_id, **kwargs) + + stage = stage.capitalize() + + # deixei aqui como um stackname explicito (ReservationApi) pq o vindo do CD é todo mal formatado e muitas vezes quem vem + # programar infra aqui pela primeira vez mal sabe de onde essa variável vem + + # SE for trocar de volta pra um stackname vindo do CD aqui, provavelmente a tabela vai ser recriada (atenção à prod) + + self.table = dynamodb.Table( + self, + id=f"ReservationApi_DynamoTable_{stage}", + table_name=f"ReservationApi_DynamoTable_{stage}", + partition_key=dynamodb.Attribute( + name="PK", + type=dynamodb.AttributeType.STRING + ), + sort_key=dynamodb.Attribute( + name="SK", + type=dynamodb.AttributeType.STRING + ), + billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST, + removal_policy=RemovalPolicy.RETAIN if stage == "Prod" else RemovalPolicy.DESTROY + ) + + + + diff --git a/iac/components/lambda_construct.py b/iac/components/lambda_construct.py new file mode 100644 index 0000000..935a2f1 --- /dev/null +++ b/iac/components/lambda_construct.py @@ -0,0 +1,251 @@ +from aws_cdk import ( + aws_lambda as lambda_, + Duration, + aws_apigateway as apigw +) +from constructs import Construct +from aws_cdk.aws_apigateway import Resource, LambdaIntegration +from aws_cdk.aws_events import Rule, Schedule, EventField, RuleTargetInput +from aws_cdk.aws_events_targets import LambdaFunction + +class LambdaConstruct(Construct): + functions_that_need_dynamo_permissions = [] + functions_that_need_s3_permissions = [] + + def create_lambda_api_gateway_integration( + self, + module_name: str, + method: str, + api_resource: Resource, + environment_variables: dict = {"STAGE": "TEST"}, + authorizer=None + ): + + function = lambda_.Function( + self, module_name.title(), + code=lambda_.Code.from_asset(f"../src/modules/{module_name}"), + handler=f"app.{module_name}_presenter.lambda_handler", + runtime=lambda_.Runtime("python3.13"), + layers=[self.lambda_layer], + environment=environment_variables, + timeout=Duration.seconds(15) + ) + + api_resource.add_resource( + module_name.replace("_", "-") + ).add_method( + method, + integration=LambdaIntegration( + function + ), + authorizer=authorizer + ) + return function + + def create_lambda_event_bridge_integration( + self, + module_name: str, + cron_schedule: Schedule.cron, + environment_variables: dict = {"STAGE": "TEST"} + ): + + function = lambda_.Function( + self, + module_name.title(), + code=lambda_.Code.from_asset(f"../src/modules/{module_name}"), + handler=f"app.{module_name}_presenter.lambda_handler", + runtime=lambda_.Runtime("python3.13"), + layers=[self.lambda_layer], + environment=environment_variables, + timeout=Duration.seconds(15) + ) + + rule = Rule( + self, f"{module_name.title()}EventRuleForWeeklyUpload", + schedule=cron_schedule + ) + + input_transformer = RuleTargetInput.from_object({ + "current_date": EventField.time, + "message": "weekly report trigger!" + }) + + rule.add_target(LambdaFunction(function, event=input_transformer)) + + return function + + def __init__( + self, + scope: Construct, + construct_id: str, + stage: str, + api_gateway_resource: Resource, + environment_variables: dict + ) -> None: + + stage = stage.capitalize() + + super().__init__(scope, f"ReservationApi_LambdaConstruct_{stage}") + + self.lambda_layer = lambda_.LayerVersion( + self, + id=f"ReservationApi_LambdaLayer_{stage}", + # a pasta .build foi obtida do adjust layer directory, certifique-se de que a configuração da pasta layer gerada la esta igual + code=lambda_.Code.from_asset("./build"), + compatible_runtimes=[lambda_.Runtime("python3.13")] + ) + + authorizer_lambda = lambda_.Function( + self, "AuthorizerUserMssReservationApiLambda", + code=lambda_.Code.from_asset("../src/shared/authorizer"), + handler="user_mss_authorizer.lambda_handler", + runtime=lambda_.Runtime("python3.13"), + layers=[self.lambda_layer], + environment=environment_variables, + timeout=Duration.seconds(15) + ) + + token_authorizer_lambda = apigw.TokenAuthorizer( + self, "TokenAuthorizerReservationApi", + handler=authorizer_lambda, + identity_source=apigw.IdentitySource.header("Authorization"), + authorizer_name="AuthorizerUserMssReservationMssAlertLambda", + results_cache_ttl=Duration.seconds(0) + ) + + self.create_booking = self.create_lambda_api_gateway_integration( + module_name="create_booking", + method="POST", + api_resource=api_gateway_resource, + environment_variables=environment_variables, + authorizer=token_authorizer_lambda + ) + + self.update_booking = self.create_lambda_api_gateway_integration( + module_name="update_booking", + method="PUT", + api_resource=api_gateway_resource, + environment_variables=environment_variables, + authorizer=token_authorizer_lambda + ) + + self.get_booking = self.create_lambda_api_gateway_integration( + module_name="get_booking", + method="GET", + api_resource=api_gateway_resource, + environment_variables=environment_variables + ) + + self.get_bookings = self.create_lambda_api_gateway_integration( + module_name="get_bookings", + method="GET", + api_resource=api_gateway_resource, + environment_variables=environment_variables, + ) + + self.delete_booking = self.create_lambda_api_gateway_integration( + module_name="delete_booking", + method="DELETE", + api_resource=api_gateway_resource, + environment_variables=environment_variables, + authorizer=token_authorizer_lambda + ) + + self.get_all_bookings = self.create_lambda_api_gateway_integration( + module_name="get_all_bookings", + method="GET", + api_resource=api_gateway_resource, + environment_variables=environment_variables + ) + + self.create_court = self.create_lambda_api_gateway_integration( + module_name="create_court", + method="POST", + api_resource=api_gateway_resource, + environment_variables=environment_variables, + authorizer=token_authorizer_lambda + ) + + self.get_court = self.create_lambda_api_gateway_integration( + module_name="get_court", + method="GET", + api_resource=api_gateway_resource, + environment_variables=environment_variables + ) + + self.update_court = self.create_lambda_api_gateway_integration( + module_name="update_court", + method="PUT", + api_resource=api_gateway_resource, + environment_variables=environment_variables, + authorizer=token_authorizer_lambda + ) + + #TODO implementar lógica de deletar foto do bucket ao excluir a quadra pelo s3_client + # @43 ;) + self.delete_court = self.create_lambda_api_gateway_integration( + module_name="delete_court", + method="DELETE", + api_resource=api_gateway_resource, + environment_variables=environment_variables, + authorizer=token_authorizer_lambda + ) + + self.get_all_courts = self.create_lambda_api_gateway_integration( + module_name="get_all_courts", + method="GET", + api_resource=api_gateway_resource, + environment_variables=environment_variables + ) + + self.health_check = self.create_lambda_api_gateway_integration( + module_name="health_check", + method="GET", + api_resource=api_gateway_resource, + environment_variables=environment_variables + ) + + #does not need auth / not a route + self.generate_report = self.create_lambda_event_bridge_integration( + module_name="generate_report", + cron_schedule=Schedule.cron(minute="30", hour="11", week_day="FRI"), + environment_variables=environment_variables + ) + + self.get_all_admin_bookings = self.create_lambda_api_gateway_integration( + module_name="get_all_admin_bookings", + method="GET", + api_resource=api_gateway_resource, + environment_variables=environment_variables + ) + + self.get_all_bookings_grouped_by_role = self.create_lambda_api_gateway_integration( + module_name="get_all_bookings_grouped_by_role", + method="GET", + api_resource=api_gateway_resource, + environment_variables=environment_variables + ) + + self.functions_that_need_dynamo_permissions = [ + self.create_court, + self.get_court, + self.update_court, + self.delete_court, + self.get_all_courts, + self.create_booking, + self.get_booking, + self.update_booking, + self.delete_booking, + self.get_all_bookings, + self.get_bookings, + self.generate_report, + self.get_all_admin_bookings, + self.get_all_bookings_grouped_by_role + ] + + self.functions_that_need_s3_permissions = [ + self.create_court, + self.update_court, + self.delete_court, + self.generate_report + ] diff --git a/iac/components/s3_construct.py b/iac/components/s3_construct.py new file mode 100644 index 0000000..942f3bc --- /dev/null +++ b/iac/components/s3_construct.py @@ -0,0 +1,43 @@ +from aws_cdk import ( + aws_s3 as s3, + aws_cloudfront as cloudfront, + aws_cloudfront_origins as origins, + RemovalPolicy, +) +from constructs import Construct +import os + +class S3Construct(Construct): + + def __init__( + self, + scope: Construct, + construct_id: str, + stage: str, + **kwargs + ) -> None: + super().__init__(scope, construct_id, **kwargs) + + # deixei aqui como um legacy para o ID stackname para o bucket nao ser recriado. no futuro quando forem atualizar + # esse stackname mesmo que seja no CD, o bucket terá de ser recriado apesar de ter o mesmo nome (creio eu) + + self.stack_name = os.environ.get("STACK_NAME") + + stage = stage.capitalize() + + self.bucket = s3.Bucket( + self, f"RESERVATION_BACK_S3_BUCKET_{stage}", + bucket_name=f"{self.stack_name}-bucket-{stage}".lower(), + versioned=True, + removal_policy=RemovalPolicy.DESTROY if not (stage == 'Prod') else RemovalPolicy.RETAIN, + block_public_access=s3.BlockPublicAccess.BLOCK_ALL + ) + + self.distribution = cloudfront.Distribution( + self, f"ReservationBucketDistribution{stage}", + default_behavior=cloudfront.BehaviorOptions( + origin=origins.S3Origin(self.bucket), + viewer_protocol_policy=cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, + ), + default_root_object=None # não obrigatório, mas evita erro se não tiver index.html + ) diff --git a/iac/components/ssm_construct.py b/iac/components/ssm_construct.py new file mode 100644 index 0000000..6bdc64e --- /dev/null +++ b/iac/components/ssm_construct.py @@ -0,0 +1,55 @@ +from constructs import Construct +from aws_cdk import Resource, aws_ssm as ssm +from aws_cdk.aws_apigateway import RestApi +from aws_cdk import aws_s3 as s3 + +class SsmConstruct(Construct): + + def __init__( + self, + scope: Construct, + construct_id: str, + stage: str, + mss_name_identification_for_path: str, + api: RestApi, + api_gateway_resource: Resource, + buckets: dict[str, s3.Bucket] = None, + extra_params: dict[str, str] = None, + **kwargs + ): + + super().__init__(scope, construct_id, **kwargs) + + # é necessário a '/' após a url pois no CD do front estamos contando como se ela ja estivesse la + + # stage lower é necessário aqui pois no actions do front, stage é recebido como lower + + stage = stage.lower() + + # aqui nao pode ter caracteres especiais, nem letras maiusculas. coloque apenas como o nome + # do microserviço tudo junto e tudo minusculo quando for passar no iac stack + + mss_name_identification_for_path = mss_name_identification_for_path.lower().replace("-", "_") + + if api: + ssm.StringParameter(self, + id=f"ApiUrl_{stage}", + parameter_name=f"/{mss_name_identification_for_path}/{stage}/api/url", + string_value=f"{api.url}{api_gateway_resource.path.lstrip('/')}/" + ) + + for logical_name, bucket in (buckets or {}).items(): + ssm.StringParameter(self, + id=f"Bucket_{logical_name}_{stage}", + parameter_name=f"/{mss_name_identification_for_path}/{stage}/buckets/{logical_name}", + string_value=bucket.bucket_name + ) + + for key, value in (extra_params or {}).items(): + safe_id = key.replace("/", "_") + ssm.StringParameter(self, + id=f"Extra_{safe_id}_{stage}", + parameter_name=f"/{mss_name_identification_for_path}/{stage}/{key}", + string_value=value + ) + \ No newline at end of file diff --git a/iac/requirements-infra.txt b/iac/requirements-infra.txt new file mode 100644 index 0000000..c4987ea --- /dev/null +++ b/iac/requirements-infra.txt @@ -0,0 +1 @@ +aws-cdk-lib==2.211.0 \ No newline at end of file diff --git a/iac/stack/__init__.py b/iac/stack/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iac/stack/iac_stack.py b/iac/stack/iac_stack.py new file mode 100644 index 0000000..4e480be --- /dev/null +++ b/iac/stack/iac_stack.py @@ -0,0 +1,116 @@ +from aws_cdk import ( + Stack, aws_iam +) +from constructs import Construct +import os + +from components.apigw_construct import ApigwConstruct +from components.s3_construct import S3Construct +from components.dynamo_construct import DynamoConstruct +from components.lambda_construct import LambdaConstruct +from components.ssm_construct import SsmConstruct + + +class IacStack(Stack): + + def __init__( + self, + scope: Construct, + stack_id: str, + **kwargs + ) -> None: + + super().__init__(scope, stack_id, **kwargs) + + self.github_ref = os.environ.get('GITHUB_REF_NAME') + stage = '' + if 'prod' in self.github_ref: + stage = 'PROD' + elif 'homolog' in self.github_ref: + stage = 'HOMOLOG' + else: + stage = 'DEV' + + stage = stage.capitalize() + + self.aws_region = os.environ.get("AWS_REGION") + + self.apigw_construct = ApigwConstruct( + self, + construct_id="ReservationApiApigw", + stage=stage, + ) + + self.dynamo_construct = DynamoConstruct( + self, + construct_id="ReservationApiDynamo", + stage=stage + ) + + self.s3_construct = S3Construct( + self, + # aqui deixei como bucket stack mesmo para não mexer nos ids lógicos e ter que recriar o bucket + # para mais explicação cheque dentro da classe + construct_id="BucketStack", + stage=stage + ) + + self.ssm_construct = SsmConstruct( + self, + construct_id="ReservationApiSsm", + stage=stage, + # atenção para esse próximo parâmetro. de preferencia deixe tudo minusculo sem _ + # isso deve corresponder ao prefixo de caminho passado no CD dos outros mss (inclusive front) + # que acessam os parametros no ssm. + mss_name_identification_for_path="reservationapi", + api=self.apigw_construct.rest_api, + api_gateway_resource=self.apigw_construct.api_gateway_resource + ) + + ENVIRONMENT_VARIABLES = { + "STAGE": stage, + "DYNAMO_TABLE_NAME": self.dynamo_construct.table.table_name, + "DYNAMO_PARTITION_KEY": "PK", + "DYNAMO_SORT_KEY": "SK", + "REGION": self.aws_region, + "USER_API_URL": os.environ.get("USER_API_URL"), + "S3_BUCKET_NAME": self.s3_construct.bucket.bucket_name, + "FROM_EMAIL": os.environ.get("FROM_EMAIL"), + "HIDDEN_COPY": os.environ.get("HIDDEN_COPY"), + "S3_ASSETS_CDN": os.environ.get("S3_ASSETS_CDN") + } + + self.lambda_construct = LambdaConstruct( + self, + construct_id="ReservationApiLambda", + stage=stage, + api_gateway_resource=self.apigw_construct.api_gateway_resource, + environment_variables=ENVIRONMENT_VARIABLES + ) + + for function in self.lambda_construct.functions_that_need_dynamo_permissions: + self.dynamo_construct.table.grant_read_write_data(function) + + for function in self.lambda_construct.functions_that_need_s3_permissions: + self.s3_construct.bucket.grant_read_write(function) + + ses_admin_policy = aws_iam.PolicyStatement( + effect=aws_iam.Effect.ALLOW, + actions=[ + "ses:*", + ], + resources=[ + "*" + ] + ) + + functions_that_need_ses_permissions = [ + self.lambda_stack.delete_booking + ] + + for f in functions_that_need_ses_permissions: + f.add_environment("HIDDEN_COPY", os.environ.get("HIDDEN_COPY")) + f.add_environment("FROM_EMAIL", os.environ.get("FROM_EMAIL")) + f.add_environment("REPLY_TO_EMAIL", os.environ.get("REPLY_TO_EMAIL")) + f.add_to_role_policy(ses_admin_policy) + diff --git a/requirements-app.txt b/requirements-app.txt new file mode 100644 index 0000000..185832d --- /dev/null +++ b/requirements-app.txt @@ -0,0 +1,10 @@ +pytest==8.4.1 +pytest-cov==6.2.1 +python-dotenv==1.1.1 +boto3==1.40.9 +aws-lambda-powertools==3.19.0 +aws_xray_sdk==2.14.0 + +pandas==2.2.3 +XlsxWriter==3.2.2 +requests==2.32.3 \ No newline at end of file From fcc23923de0b184b00116754713f6139bc9a0295 Mon Sep 17 00:00:00 2001 From: Leo Iorio Date: Mon, 30 Mar 2026 11:46:34 -0300 Subject: [PATCH 03/15] updated ci python version --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 4d12145..615e003 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -7,4 +7,4 @@ on: jobs: testApp: - uses: maua-dev/ci_workflows_reusable/.github/workflows/pytest_ci.yml@V1.0 + uses: maua-dev/ci_workflows_reusable/.github/workflows/pytest_ci_311.yml@V1.2 From d9d6315d6a4bee49aa3a8b885a483653901234f1 Mon Sep 17 00:00:00 2001 From: Leo Iorio Date: Mon, 30 Mar 2026 11:49:29 -0300 Subject: [PATCH 04/15] fixing --- iac/app.py | 2 +- iac/stacks/__init__.py | 0 iac/stacks/bucket_stack.py | 43 ------- iac/stacks/dynamo_stack.py | 40 ------ iac/stacks/iac_stack.py | 132 -------------------- iac/stacks/lambda_stack.py | 243 ------------------------------------- 6 files changed, 1 insertion(+), 459 deletions(-) delete mode 100644 iac/stacks/__init__.py delete mode 100644 iac/stacks/bucket_stack.py delete mode 100644 iac/stacks/dynamo_stack.py delete mode 100644 iac/stacks/iac_stack.py delete mode 100644 iac/stacks/lambda_stack.py diff --git a/iac/app.py b/iac/app.py index 6e19da3..a436003 100644 --- a/iac/app.py +++ b/iac/app.py @@ -3,7 +3,7 @@ import aws_cdk as cdk from adjust_layer_directory import adjust_layer_directory -from stacks.iac_stack import IacStack +from stack.iac_stack import IacStack diff --git a/iac/stacks/__init__.py b/iac/stacks/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/iac/stacks/bucket_stack.py b/iac/stacks/bucket_stack.py deleted file mode 100644 index 2e28d8d..0000000 --- a/iac/stacks/bucket_stack.py +++ /dev/null @@ -1,43 +0,0 @@ -from aws_cdk import ( - aws_s3 as s3, - aws_cloudfront as cloudfront, - aws_cloudfront_origins as origins, - aws_iam as iam, - RemovalPolicy, -) -from constructs import Construct -import os - -class BucketStack(Construct): - - def __init__(self, scope: Construct, **kwargs) -> None: - super().__init__(scope, "BucketStack", **kwargs) - - self.github_ref = os.environ.get('GITHUB_REF_NAME') - self.stack_name = os.environ.get("STACK_NAME") - - stage = '' - if 'prod' in self.github_ref: - stage = 'PROD' - elif 'homolog' in self.github_ref: - stage = 'HOMOLOG' - else: - stage = 'DEV' - - self.bucket = s3.Bucket( - self, f"RESERVATION_BACK_S3_BUCKET_{stage}", - # TODO remover isso quando voltar pra conta nova ou tentar deletar o bucket criado la com power user - bucket_name=f"{self.stack_name}-bucket-{stage}".lower(), - versioned=True, - removal_policy=RemovalPolicy.DESTROY if not (stage == 'PROD') else RemovalPolicy.RETAIN, - block_public_access=s3.BlockPublicAccess.BLOCK_ALL - ) - - self.distribution = cloudfront.Distribution( - self, f"ReservationBucketDistribution{stage}", - default_behavior=cloudfront.BehaviorOptions( - origin=origins.S3Origin(self.bucket), - viewer_protocol_policy=cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, - ), - default_root_object=None # não obrigatório, mas evita erro se não tiver index.html - ) diff --git a/iac/stacks/dynamo_stack.py b/iac/stacks/dynamo_stack.py deleted file mode 100644 index ba5680e..0000000 --- a/iac/stacks/dynamo_stack.py +++ /dev/null @@ -1,40 +0,0 @@ -from aws_cdk import ( - aws_dynamodb as dynamodb, RemovalPolicy, -) -from constructs import Construct -import os - -class DynamoStack(Construct): - table: dynamodb.Table - - def __init__(self, scope: Construct, **kwargs) -> None: - super().__init__(scope, "DynamoStack",**kwargs) - - self.github_ref = os.environ.get('GITHUB_REF_NAME') - self.stack_name = os.environ.get("STACK_NAME") - - stage = '' - if 'prod' in self.github_ref: - stage = 'PROD' - elif 'homolog' in self.github_ref: - stage = 'HOMOLOG' - else: - stage = 'DEV' - - self.table = dynamodb.Table( - self, f"{self.stack_name}_DynamoTable_{stage}", - partition_key=dynamodb.Attribute( - name="PK", - type=dynamodb.AttributeType.STRING - ), - sort_key=dynamodb.Attribute( - name="SK", - type=dynamodb.AttributeType.STRING - ), - billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST, - removal_policy=RemovalPolicy.DESTROY - ) - - - - diff --git a/iac/stacks/iac_stack.py b/iac/stacks/iac_stack.py deleted file mode 100644 index e70c591..0000000 --- a/iac/stacks/iac_stack.py +++ /dev/null @@ -1,132 +0,0 @@ -from aws_cdk import ( - Stack, aws_iam -) -from constructs import Construct -from aws_cdk.aws_apigateway import RestApi, Cors, CorsOptions, GatewayResponse, ResponseType -import os - -from .bucket_stack import BucketStack -from .lambda_stack import LambdaStack -from .dynamo_stack import DynamoStack - - -class IacStack(Stack): - - def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: - super().__init__(scope, construct_id, **kwargs) - - self.github_ref = os.environ.get('GITHUB_REF_NAME') - stage = '' - if 'prod' in self.github_ref: - stage = 'PROD' - elif 'homolog' in self.github_ref: - stage = 'HOMOLOG' - else: - stage = 'DEV' - - self.aws_region = os.environ.get("AWS_REGION") - stack_name = os.environ.get("STACK_NAME") - - cors_options = CorsOptions( - allow_origins = - [ - "https://reservation.maua.br", - "https://reservation.devmaua.com" - ] - if stage == 'PROD' - else - [ - "https://reservation.hml.devmaua.com", - "https://reservation.dev.devmaua.com", - "https://localhost:3000", - "http://localhost:3000" - ], - allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], - allow_headers=Cors.DEFAULT_HEADERS - ) - - self.rest_api = RestApi( - self, f"{stack_name}_RestApi_{stage}", - rest_api_name=f"{stack_name}_RestApi_{stage}", - description="This is the Maua Reservation RestApi", - default_cors_preflight_options=cors_options - ) - - GatewayResponse( - self, - "AuthorizerDenyResponse", - rest_api=self.rest_api, - type=ResponseType.ACCESS_DENIED, - response_headers={ - "Access-Control-Allow-Origin": "'*'", - "Access-Control-Allow-Headers": "'*'", - "Access-Control-Allow-Methods": "'*'", - }, - status_code="403" - ) - - GatewayResponse( - self, - "AuthorizerUnauthorizedResponse", - rest_api=self.rest_api, - type=ResponseType.UNAUTHORIZED, - response_headers={ - "Access-Control-Allow-Origin": "'*'", - "Access-Control-Allow-Headers": "'*'", - "Access-Control-Allow-Methods": "'*'", - }, - status_code="401" - ) - - api_gateway_resource = self.rest_api.root.add_resource( - "reservation-api", - default_cors_preflight_options=cors_options - ) - - self.dynamo_table = DynamoStack(self) - self.s3_bucket = BucketStack(self) - - ENVIRONMENT_VARIABLES = { - "STAGE": stage, - "DYNAMO_TABLE_NAME": self.dynamo_table.table.table_name, - "DYNAMO_PARTITION_KEY": "PK", - "DYNAMO_SORT_KEY": "SK", - "REGION": self.aws_region, - "USER_API_URL": os.environ.get("USER_API_URL"), - "S3_BUCKET_NAME": self.s3_bucket.bucket.bucket_name, - "FROM_EMAIL": os.environ.get("FROM_EMAIL"), - "HIDDEN_COPY": os.environ.get("HIDDEN_COPY"), - "S3_ASSETS_CDN": os.environ.get("S3_ASSETS_CDN") - } - - - - self.lambda_stack = LambdaStack(self, api_gateway_resource=api_gateway_resource, - environment_variables=ENVIRONMENT_VARIABLES) - - for function in self.lambda_stack.functions_that_need_dynamo_permissions: - self.dynamo_table.table.grant_read_write_data(function) - - for function in self.lambda_stack.functions_that_need_s3_permissions: - self.s3_bucket.bucket.grant_read_write(function) - - ses_admin_policy = aws_iam.PolicyStatement( - effect=aws_iam.Effect.ALLOW, - actions=[ - "ses:*", - ], - resources=[ - "*" - ] - ) - - functions_that_need_ses_permissions = [ - self.lambda_stack.delete_booking - ] - - for f in functions_that_need_ses_permissions: - f.add_environment("HIDDEN_COPY", os.environ.get("HIDDEN_COPY")) - f.add_environment("FROM_EMAIL", os.environ.get("FROM_EMAIL")) - f.add_environment("REPLY_TO_EMAIL", os.environ.get("REPLY_TO_EMAIL")) - f.add_to_role_policy(ses_admin_policy) - diff --git a/iac/stacks/lambda_stack.py b/iac/stacks/lambda_stack.py deleted file mode 100644 index dbf1138..0000000 --- a/iac/stacks/lambda_stack.py +++ /dev/null @@ -1,243 +0,0 @@ -import os - -from aws_cdk import ( - aws_lambda as lambda_, - NestedStack, Duration, - aws_apigateway as apigw -) -from constructs import Construct -from aws_cdk.aws_apigateway import Resource, LambdaIntegration -from aws_cdk.aws_events import Rule, Schedule, EventField, RuleTargetInput -from aws_cdk.aws_events_targets import LambdaFunction - -class LambdaStack(Construct): - functions_that_need_dynamo_permissions = [] - - def create_lambda_api_gateway_integration(self, module_name: str, method: str, api_resource: Resource, - environment_variables: dict = {"STAGE": "TEST"}, authorizer=None): - function = lambda_.Function( - self, module_name.title(), - code=lambda_.Code.from_asset(f"../src/modules/{module_name}"), - handler=f"app.{module_name}_presenter.lambda_handler", - runtime=lambda_.Runtime("python3.13"), - layers=[self.lambda_layer], - environment=environment_variables, - timeout=Duration.seconds(15) - ) - - api_resource.add_resource(module_name.replace("_", "-")).add_method(method, - integration=LambdaIntegration( - function), - authorizer=authorizer) - - return function - - def create_lambda_event_bridge_integration(self, - module_name: str, - cron_schedule: Schedule.cron, - environment_variables: dict = {"STAGE": "TEST"}): - function = lambda_.Function( - self, - module_name.title(), - code=lambda_.Code.from_asset(f"../src/modules/{module_name}"), - handler=f"app.{module_name}_presenter.lambda_handler", - runtime=lambda_.Runtime("python3.13"), - layers=[self.lambda_layer], - environment=environment_variables, - timeout=Duration.seconds(15) - ) - - rule = Rule( - self, f"{module_name.title()}EventRuleForWeeklyUpload", - schedule=cron_schedule - ) - - input_transformer = RuleTargetInput.from_object({ - "current_date": EventField.time, - "message": "weekly report trigger!" - }) - - rule.add_target(LambdaFunction(function, event=input_transformer)) - - return function - - def __init__(self, scope: Construct, api_gateway_resource: Resource, environment_variables: dict) -> None: - self.github_ref = os.environ.get('GITHUB_REF_NAME') - self.stack_name = os.environ.get("STACK_NAME") - stage = '' - if 'prod' in self.github_ref: - stage = 'PROD' - elif 'homolog' in self.github_ref: - stage = 'HOMOLOG' - else: - stage = 'DEV' - - super().__init__(scope, f"{self.stack_name}_LambdaStack_{stage}") - - self.lambda_layer = lambda_.LayerVersion(self, f"{self.stack_name}_Lambda_Layer_{stage}", - code=lambda_.Code.from_asset("./build"), - compatible_runtimes=[lambda_.Runtime("python3.13")] - ) - - authorizer_lambda = lambda_.Function( - self, "AuthorizerUserMssReservationApiLambda", - code=lambda_.Code.from_asset("../src/shared/authorizer"), - handler="user_mss_authorizer.lambda_handler", - runtime=lambda_.Runtime("python3.13"), - layers=[self.lambda_layer], - environment=environment_variables, - timeout=Duration.seconds(15) - ) - - token_authorizer_lambda = apigw.TokenAuthorizer( - self, "TokenAuthorizerReservationApi", - handler=authorizer_lambda, - identity_source=apigw.IdentitySource.header("Authorization"), - authorizer_name="AuthorizerUserMssReservationMssAlertLambda", - results_cache_ttl=Duration.seconds(0) - ) - - #ready for auth - self.create_booking = self.create_lambda_api_gateway_integration( - module_name="create_booking", - method="POST", - api_resource=api_gateway_resource, - environment_variables=environment_variables, - authorizer=token_authorizer_lambda - ) - - #ready for auth - self.update_booking = self.create_lambda_api_gateway_integration( - module_name="update_booking", - method="PUT", - api_resource=api_gateway_resource, - environment_variables=environment_variables, - authorizer=token_authorizer_lambda - ) - - #not ready for auth AND not used? - self.get_booking = self.create_lambda_api_gateway_integration( - module_name="get_booking", - method="GET", - api_resource=api_gateway_resource, - environment_variables=environment_variables - ) - - self.get_bookings = self.create_lambda_api_gateway_integration( - module_name="get_bookings", - method="GET", - api_resource=api_gateway_resource, - environment_variables=environment_variables, - ) - - #not ready for auth??TODO - self.delete_booking = self.create_lambda_api_gateway_integration( - module_name="delete_booking", - method="DELETE", - api_resource=api_gateway_resource, - environment_variables=environment_variables, - authorizer=token_authorizer_lambda - ) - - #not auth and unused - self.get_all_bookings = self.create_lambda_api_gateway_integration( - module_name="get_all_bookings", - method="GET", - api_resource=api_gateway_resource, - environment_variables=environment_variables - ) - - #ready for auth - self.create_court = self.create_lambda_api_gateway_integration( - module_name="create_court", - method="POST", - api_resource=api_gateway_resource, - environment_variables=environment_variables, - authorizer=token_authorizer_lambda - ) - - #not ready TODO - self.get_court = self.create_lambda_api_gateway_integration( - module_name="get_court", - method="GET", - api_resource=api_gateway_resource, - environment_variables=environment_variables - ) - - #ready - self.update_court = self.create_lambda_api_gateway_integration( - module_name="update_court", - method="PUT", - api_resource=api_gateway_resource, - environment_variables=environment_variables, - authorizer=token_authorizer_lambda - ) - - #ready - self.delete_court = self.create_lambda_api_gateway_integration( - module_name="delete_court", - method="DELETE", - api_resource=api_gateway_resource, - environment_variables=environment_variables, - authorizer=token_authorizer_lambda - ) - - #not ready? needed? - self.get_all_courts = self.create_lambda_api_gateway_integration( - module_name="get_all_courts", - method="GET", - api_resource=api_gateway_resource, - environment_variables=environment_variables - ) - - #yes - self.health_check = self.create_lambda_api_gateway_integration( - module_name="health_check", - method="GET", - api_resource=api_gateway_resource, - environment_variables=environment_variables - ) - - #does not need auth / not a route - self.generate_report = self.create_lambda_event_bridge_integration( - module_name="generate_report", - cron_schedule=Schedule.cron(minute="30", hour="11", week_day="FRI"), - environment_variables=environment_variables - ) - - self.get_all_admin_bookings = self.create_lambda_api_gateway_integration( - module_name="get_all_admin_bookings", - method="GET", - api_resource=api_gateway_resource, - environment_variables=environment_variables - ) - - self.get_all_bookings_grouped_by_role = self.create_lambda_api_gateway_integration( - module_name="get_all_bookings_grouped_by_role", - method="GET", - api_resource=api_gateway_resource, - environment_variables=environment_variables - ) - - self.functions_that_need_dynamo_permissions = [ - self.create_court, - self.get_court, - self.update_court, - self.delete_court, - self.get_all_courts, - self.create_booking, - self.get_booking, - self.update_booking, - self.delete_booking, - self.get_all_bookings, - self.get_bookings, - self.generate_report, - self.get_all_admin_bookings, - self.get_all_bookings_grouped_by_role - ] - - self.functions_that_need_s3_permissions = [ - self.create_court, - self.update_court, - self.generate_report - ] From 43e8546682f2524eee82e8e97d26e8095985fe6d Mon Sep 17 00:00:00 2001 From: Leo Iorio Date: Mon, 30 Mar 2026 11:51:31 -0300 Subject: [PATCH 05/15] fixing typo --- iac/stack/iac_stack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iac/stack/iac_stack.py b/iac/stack/iac_stack.py index 4e480be..240d2da 100644 --- a/iac/stack/iac_stack.py +++ b/iac/stack/iac_stack.py @@ -105,7 +105,7 @@ def __init__( ) functions_that_need_ses_permissions = [ - self.lambda_stack.delete_booking + self.lambda_construct.delete_booking ] for f in functions_that_need_ses_permissions: From 067aaa7b22d699d833f65a923bc1697476be6f21 Mon Sep 17 00:00:00 2001 From: Leo Iorio Date: Mon, 30 Mar 2026 12:01:00 -0300 Subject: [PATCH 06/15] adjusting logical ids for s3 as bucket is beeing recreated anyway --- iac/components/s3_construct.py | 15 +++++---------- iac/stack/iac_stack.py | 4 +--- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/iac/components/s3_construct.py b/iac/components/s3_construct.py index 942f3bc..020aa38 100644 --- a/iac/components/s3_construct.py +++ b/iac/components/s3_construct.py @@ -5,7 +5,6 @@ RemovalPolicy, ) from constructs import Construct -import os class S3Construct(Construct): @@ -18,23 +17,19 @@ def __init__( ) -> None: super().__init__(scope, construct_id, **kwargs) - # deixei aqui como um legacy para o ID stackname para o bucket nao ser recriado. no futuro quando forem atualizar - # esse stackname mesmo que seja no CD, o bucket terá de ser recriado apesar de ter o mesmo nome (creio eu) - - self.stack_name = os.environ.get("STACK_NAME") - stage = stage.capitalize() - self.bucket = s3.Bucket( - self, f"RESERVATION_BACK_S3_BUCKET_{stage}", - bucket_name=f"{self.stack_name}-bucket-{stage}".lower(), + self.bucket_spreadsheets = s3.Bucket( + self, + id=f"ReservationApi_Bucket_Back_{stage}", + bucket_name=f"ReservationApi-spreadsheets-{stage}".lower(), versioned=True, removal_policy=RemovalPolicy.DESTROY if not (stage == 'Prod') else RemovalPolicy.RETAIN, block_public_access=s3.BlockPublicAccess.BLOCK_ALL ) self.distribution = cloudfront.Distribution( - self, f"ReservationBucketDistribution{stage}", + self, f"ReservationApiSpreadsheetsBucketDistribution{stage}", default_behavior=cloudfront.BehaviorOptions( origin=origins.S3Origin(self.bucket), viewer_protocol_policy=cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, diff --git a/iac/stack/iac_stack.py b/iac/stack/iac_stack.py index 240d2da..3a265dc 100644 --- a/iac/stack/iac_stack.py +++ b/iac/stack/iac_stack.py @@ -49,9 +49,7 @@ def __init__( self.s3_construct = S3Construct( self, - # aqui deixei como bucket stack mesmo para não mexer nos ids lógicos e ter que recriar o bucket - # para mais explicação cheque dentro da classe - construct_id="BucketStack", + construct_id="ReservationApiS3", stage=stage ) From befee1057369d2fd824f55d71aac24a6418294e1 Mon Sep 17 00:00:00 2001 From: Leo Iorio Date: Mon, 30 Mar 2026 12:02:43 -0300 Subject: [PATCH 07/15] fixing typos --- iac/stack/iac_stack.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/iac/stack/iac_stack.py b/iac/stack/iac_stack.py index 3a265dc..2a4eb4f 100644 --- a/iac/stack/iac_stack.py +++ b/iac/stack/iac_stack.py @@ -72,7 +72,7 @@ def __init__( "DYNAMO_SORT_KEY": "SK", "REGION": self.aws_region, "USER_API_URL": os.environ.get("USER_API_URL"), - "S3_BUCKET_NAME": self.s3_construct.bucket.bucket_name, + "S3_BUCKET_NAME": self.s3_construct.bucket_spreadsheets.bucket_name, "FROM_EMAIL": os.environ.get("FROM_EMAIL"), "HIDDEN_COPY": os.environ.get("HIDDEN_COPY"), "S3_ASSETS_CDN": os.environ.get("S3_ASSETS_CDN") @@ -90,7 +90,7 @@ def __init__( self.dynamo_construct.table.grant_read_write_data(function) for function in self.lambda_construct.functions_that_need_s3_permissions: - self.s3_construct.bucket.grant_read_write(function) + self.s3_construct.bucket_spreadsheets.grant_read_write(function) ses_admin_policy = aws_iam.PolicyStatement( effect=aws_iam.Effect.ALLOW, From 0596dbe39f536d341e138299b2bc2b4cdbb64172 Mon Sep 17 00:00:00 2001 From: Leo Iorio Date: Mon, 30 Mar 2026 12:06:21 -0300 Subject: [PATCH 08/15] fixing typos 2 --- iac/components/s3_construct.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iac/components/s3_construct.py b/iac/components/s3_construct.py index 020aa38..4586171 100644 --- a/iac/components/s3_construct.py +++ b/iac/components/s3_construct.py @@ -31,7 +31,7 @@ def __init__( self.distribution = cloudfront.Distribution( self, f"ReservationApiSpreadsheetsBucketDistribution{stage}", default_behavior=cloudfront.BehaviorOptions( - origin=origins.S3Origin(self.bucket), + origin=origins.S3Origin(self.bucket_spreadsheets), viewer_protocol_policy=cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, ), default_root_object=None # não obrigatório, mas evita erro se não tiver index.html From 66466e9154f68eb643b67ccdc16b7862364bf788 Mon Sep 17 00:00:00 2001 From: Leo Iorio Date: Mon, 30 Mar 2026 16:05:40 -0300 Subject: [PATCH 09/15] removing / from url request in authorizer, as new url parameter already comes with it --- src/shared/authorizer/user_mss_authorizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/authorizer/user_mss_authorizer.py b/src/shared/authorizer/user_mss_authorizer.py index b250dc1..21da681 100644 --- a/src/shared/authorizer/user_mss_authorizer.py +++ b/src/shared/authorizer/user_mss_authorizer.py @@ -34,7 +34,7 @@ def lambda_handler(event, context): # Fetching the user information from the user mss methodArn = event["methodArn"] headers = {"Authorization": f"Bearer {token}"} - response = http.request("GET", MSS_USER_API_ENDPOINT + "/reservation-mss-user/get-user", headers=headers) + response = http.request("GET", MSS_USER_API_ENDPOINT + "get-user", headers=headers) # Checking if the request was successful if response.status != 200: From 5c5893a05f362a2f764f73b3e2bd2124d28abb47 Mon Sep 17 00:00:00 2001 From: Leo Iorio Date: Mon, 30 Mar 2026 16:15:23 -0300 Subject: [PATCH 10/15] excluding requirements files and updating adjust layer --- .github/workflows/CD.yml | 2 +- iac/adjust_layer_directory.py | 2 +- iac/requirements-dev.txt | 1 - iac/stack/iac_stack.py | 3 ++- requirements-dev.txt | 10 ---------- requirements-layer.txt | 7 ------- 6 files changed, 4 insertions(+), 21 deletions(-) delete mode 100644 iac/requirements-dev.txt delete mode 100644 requirements-dev.txt delete mode 100644 requirements-layer.txt diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index cf40783..6241973 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -103,7 +103,7 @@ jobs: run: | npm install -g aws-cdk cd iac - pip install -r requirements.txt + pip install -r requirements-infra.txt - name: DeployWithCDK run: | diff --git a/iac/adjust_layer_directory.py b/iac/adjust_layer_directory.py index 61845b2..ef1ad0a 100644 --- a/iac/adjust_layer_directory.py +++ b/iac/adjust_layer_directory.py @@ -6,7 +6,7 @@ # --- Configurações --- BUILD_DIRECTORY = "build" PYTHON_TOP_LEVEL_DIR = os.path.join(BUILD_DIRECTORY, "python") -REQUIREMENTS_FILE = "requirements-layer.txt" +REQUIREMENTS_FILE = "requirements-app.txt" # --- CONSTRUÇÃO CORRETA DO CAMINHO --- # Pega o diretório do projeto (a raiz 'reservation_api') subindo um nível a partir do script atual. diff --git a/iac/requirements-dev.txt b/iac/requirements-dev.txt deleted file mode 100644 index 9270945..0000000 --- a/iac/requirements-dev.txt +++ /dev/null @@ -1 +0,0 @@ -pytest==6.2.5 diff --git a/iac/stack/iac_stack.py b/iac/stack/iac_stack.py index 2a4eb4f..80bfd75 100644 --- a/iac/stack/iac_stack.py +++ b/iac/stack/iac_stack.py @@ -66,7 +66,8 @@ def __init__( ) ENVIRONMENT_VARIABLES = { - "STAGE": stage, + # o .upper() aqui existe por causa do enum em environments usado para o stage + "STAGE": stage.upper(), "DYNAMO_TABLE_NAME": self.dynamo_construct.table.table_name, "DYNAMO_PARTITION_KEY": "PK", "DYNAMO_SORT_KEY": "SK", diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 2a9eb03..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,10 +0,0 @@ -pytest==6.2.5 -pytest-cov==4.0.0 -boto3==1.24.88 -python-dotenv==0.21.0 -aws-lambda-powertools==2.9.0 -aws_xray_sdk==2.11.0 - -pandas==2.2.3 -XlsxWriter==3.2.2 -requests==2.32.3 \ No newline at end of file diff --git a/requirements-layer.txt b/requirements-layer.txt deleted file mode 100644 index d6f5bfe..0000000 --- a/requirements-layer.txt +++ /dev/null @@ -1,7 +0,0 @@ -#boto3 compatibility -urllib3<1.27 - -#lambda needs -pandas==2.2.3 -XlsxWriter==3.2.2 -requests==2.32.3 \ No newline at end of file From 4d5725049a2d9dd443033402a95e8fe833e0da4a Mon Sep 17 00:00:00 2001 From: Leo Iorio Date: Tue, 31 Mar 2026 13:15:01 -0300 Subject: [PATCH 11/15] updating bucket name to be unique, improving naming in lambda and logical ids, adding stack name pattern to ids and names (stage as well was defaulted to be capitalized in app.py) --- .github/workflows/CD.yml | 2 +- iac/app.py | 29 ++++++++++---------- iac/components/apigw_construct.py | 10 ++++--- iac/components/dynamo_construct.py | 12 +++----- iac/components/lambda_construct.py | 38 ++++++++++++++++++-------- iac/components/s3_construct.py | 6 ++-- iac/components/ssm_construct.py | 2 ++ iac/stack/iac_stack.py | 44 ++++++++++++------------------ 8 files changed, 76 insertions(+), 67 deletions(-) diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index 6241973..7822c73 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -55,7 +55,7 @@ jobs: else echo "Invalid branch name!" && exit 1 fi - echo "STACK_NAME=ReservationStackScheduleANDCourts${{env.STAGE}}" >> $GITHUB_ENV + echo "STACK_NAME=ReservationApiStack" >> $GITHUB_ENV - name: Fetch config from SSM run: | diff --git a/iac/app.py b/iac/app.py index a436003..6739bd3 100644 --- a/iac/app.py +++ b/iac/app.py @@ -19,26 +19,27 @@ aws_region = os.environ.get("AWS_REGION") aws_account_id = os.environ.get("AWS_ACCOUNT_ID") stack_name = os.environ.get("STACK_NAME") -github_ref = os.environ.get("GITHUB_REF_NAME") - -stage = '' -if 'prod' in github_ref: - stage = 'PROD' -elif 'homolog' in github_ref: - stage = 'HOMOLOG' -elif 'dev' in github_ref: - stage = 'DEV' -else: - stage = 'TEST' + +# CD ja checa se esta dentro do branch name dev, hmlg, prod +# É ideal manter o CD como capitalize para seguir no mesmo esquema do stackname +stage = os.environ.get("GITHUB_REF_NAME").capitalize() tags = { - 'project': 'Reservation Courts and Schedule MSS', + 'project': 'Reservation Api MSS', 'stage': stage, - 'stack': 'BACK', + 'stack': stack_name, 'owner': 'DevCommunity' } -IacStack(app, stack_name, env=cdk.Environment(account=aws_account_id, region=aws_region), tags=tags) +IacStack( + app, + stack_id=stack_name, + env=cdk.Environment( + account=aws_account_id, + region=aws_region + ), + tags=tags +) app.synth() diff --git a/iac/components/apigw_construct.py b/iac/components/apigw_construct.py index 3e4cc89..4a48446 100644 --- a/iac/components/apigw_construct.py +++ b/iac/components/apigw_construct.py @@ -11,12 +11,13 @@ def __init__( self, scope: Construct, construct_id: str, - stage: str, + stage: str, + stack_name: str, **kwargs ): stage = stage.capitalize() - + super().__init__(scope, construct_id, **kwargs) cors_options = CorsOptions( @@ -38,8 +39,9 @@ def __init__( ) self.rest_api = RestApi( - self, f"ReservationApi_RestApi_{stage}", - rest_api_name=f"ReservationApi_RestApi_{stage}", + self, + id=f"{stack_name}_RestApi_{stage}", + rest_api_name=f"{stack_name}_RestApi_{stage}", description="This is the Maua Reservation RestApi", deploy_options=apigateway.StageOptions( stage_name=stage.lower(), diff --git a/iac/components/dynamo_construct.py b/iac/components/dynamo_construct.py index a753ae1..1ad288e 100644 --- a/iac/components/dynamo_construct.py +++ b/iac/components/dynamo_construct.py @@ -10,22 +10,18 @@ def __init__( self, scope: Construct, construct_id: str, - stage: str, + stage: str, + stack_name: str, **kwargs ) -> None: super().__init__(scope, construct_id, **kwargs) stage = stage.capitalize() - - # deixei aqui como um stackname explicito (ReservationApi) pq o vindo do CD é todo mal formatado e muitas vezes quem vem - # programar infra aqui pela primeira vez mal sabe de onde essa variável vem - - # SE for trocar de volta pra um stackname vindo do CD aqui, provavelmente a tabela vai ser recriada (atenção à prod) self.table = dynamodb.Table( self, - id=f"ReservationApi_DynamoTable_{stage}", - table_name=f"ReservationApi_DynamoTable_{stage}", + id=f"{stack_name}_DynamoTable_{stage}", + table_name=f"{stack_name}_DynamoTable_{stage}", partition_key=dynamodb.Attribute( name="PK", type=dynamodb.AttributeType.STRING diff --git a/iac/components/lambda_construct.py b/iac/components/lambda_construct.py index 935a2f1..74a6563 100644 --- a/iac/components/lambda_construct.py +++ b/iac/components/lambda_construct.py @@ -9,8 +9,10 @@ from aws_cdk.aws_events_targets import LambdaFunction class LambdaConstruct(Construct): - functions_that_need_dynamo_permissions = [] - functions_that_need_s3_permissions = [] + functions_that_need_dynamo_permissions: list + functions_that_need_s3_permissions: list + stage: str + stack_name: str def create_lambda_api_gateway_integration( self, @@ -22,7 +24,9 @@ def create_lambda_api_gateway_integration( ): function = lambda_.Function( - self, module_name.title(), + self, + id=module_name.title(), + function_name=f"{module_name}-{self.stack_name}-{self.stage}", code=lambda_.Code.from_asset(f"../src/modules/{module_name}"), handler=f"app.{module_name}_presenter.lambda_handler", runtime=lambda_.Runtime("python3.13"), @@ -51,7 +55,8 @@ def create_lambda_event_bridge_integration( function = lambda_.Function( self, - module_name.title(), + id=module_name.title(), + function_name=f"{module_name}-{self.stack_name}-{self.stage}", code=lambda_.Code.from_asset(f"../src/modules/{module_name}"), handler=f"app.{module_name}_presenter.lambda_handler", runtime=lambda_.Runtime("python3.13"), @@ -61,7 +66,9 @@ def create_lambda_event_bridge_integration( ) rule = Rule( - self, f"{module_name.title()}EventRuleForWeeklyUpload", + self, + id=f"{module_name.title()}EventRuleForWeeklyUpload", + rule_name=f"{module_name.title()}EventRuleForWeeklyUpload", schedule=cron_schedule ) @@ -79,24 +86,30 @@ def __init__( scope: Construct, construct_id: str, stage: str, + stack_name: str, api_gateway_resource: Resource, - environment_variables: dict + environment_variables: dict, + **kargs ) -> None: - stage = stage.capitalize() + self.stage = stage + self.stack_name = stack_name - super().__init__(scope, f"ReservationApi_LambdaConstruct_{stage}") + super().__init__(scope, construct_id, **kargs) self.lambda_layer = lambda_.LayerVersion( self, - id=f"ReservationApi_LambdaLayer_{stage}", + id=f"{stack_name}_LambdaLayer_{stage}", + layer_version_name=f"{stack_name}-LambdaLayer-{self.stage}", # a pasta .build foi obtida do adjust layer directory, certifique-se de que a configuração da pasta layer gerada la esta igual code=lambda_.Code.from_asset("./build"), compatible_runtimes=[lambda_.Runtime("python3.13")] ) authorizer_lambda = lambda_.Function( - self, "AuthorizerUserMssReservationApiLambda", + self, + id=f"LambdaUserMssAuthorizer-{self.stack_name}-{self.stage}", + function_name=f"lambda_user_mss_authorizer-{self.stack_name}-{self.stage}", code=lambda_.Code.from_asset("../src/shared/authorizer"), handler="user_mss_authorizer.lambda_handler", runtime=lambda_.Runtime("python3.13"), @@ -106,10 +119,11 @@ def __init__( ) token_authorizer_lambda = apigw.TokenAuthorizer( - self, "TokenAuthorizerReservationApi", + self, + id=f"TokenUserMssAuthorizer-{self.stack_name}-{self.stage}", + authorizer_name=f"user_mss_authorizer-{self.stack_name}-{self.stage}", handler=authorizer_lambda, identity_source=apigw.IdentitySource.header("Authorization"), - authorizer_name="AuthorizerUserMssReservationMssAlertLambda", results_cache_ttl=Duration.seconds(0) ) diff --git a/iac/components/s3_construct.py b/iac/components/s3_construct.py index 4586171..143de8e 100644 --- a/iac/components/s3_construct.py +++ b/iac/components/s3_construct.py @@ -3,6 +3,7 @@ aws_cloudfront as cloudfront, aws_cloudfront_origins as origins, RemovalPolicy, + Aws ) from constructs import Construct @@ -12,7 +13,8 @@ def __init__( self, scope: Construct, construct_id: str, - stage: str, + stage: str, + stack_name: str, **kwargs ) -> None: super().__init__(scope, construct_id, **kwargs) @@ -22,7 +24,7 @@ def __init__( self.bucket_spreadsheets = s3.Bucket( self, id=f"ReservationApi_Bucket_Back_{stage}", - bucket_name=f"ReservationApi-spreadsheets-{stage}".lower(), + bucket_name=f"reservationapi-spreadsheets-{stage.lower()}-{Aws.ACCOUNT_ID}-{Aws.REGION}", versioned=True, removal_policy=RemovalPolicy.DESTROY if not (stage == 'Prod') else RemovalPolicy.RETAIN, block_public_access=s3.BlockPublicAccess.BLOCK_ALL diff --git a/iac/components/ssm_construct.py b/iac/components/ssm_construct.py index 6bdc64e..be37ff1 100644 --- a/iac/components/ssm_construct.py +++ b/iac/components/ssm_construct.py @@ -29,6 +29,8 @@ def __init__( # aqui nao pode ter caracteres especiais, nem letras maiusculas. coloque apenas como o nome # do microserviço tudo junto e tudo minusculo quando for passar no iac stack + # podemos estudar passar o stackname como esse caminho. porem fiquemos atentos a caracteres especiais e letras maiusculas + mss_name_identification_for_path = mss_name_identification_for_path.lower().replace("-", "_") if api: diff --git a/iac/stack/iac_stack.py b/iac/stack/iac_stack.py index 80bfd75..81aaf59 100644 --- a/iac/stack/iac_stack.py +++ b/iac/stack/iac_stack.py @@ -1,5 +1,5 @@ from aws_cdk import ( - Stack, aws_iam + Stack, aws_iam, Aws ) from constructs import Construct import os @@ -16,46 +16,38 @@ class IacStack(Stack): def __init__( self, scope: Construct, - stack_id: str, + stack_id: str, + stage: str, + stack_name: str, **kwargs ) -> None: super().__init__(scope, stack_id, **kwargs) - - self.github_ref = os.environ.get('GITHUB_REF_NAME') - stage = '' - if 'prod' in self.github_ref: - stage = 'PROD' - elif 'homolog' in self.github_ref: - stage = 'HOMOLOG' - else: - stage = 'DEV' - - stage = stage.capitalize() - - self.aws_region = os.environ.get("AWS_REGION") - + self.apigw_construct = ApigwConstruct( self, - construct_id="ReservationApiApigw", + construct_id=f"{stack_name}-Apigw", stage=stage, + stack_name=stack_name ) self.dynamo_construct = DynamoConstruct( self, - construct_id="ReservationApiDynamo", - stage=stage + construct_id=f"{stack_name}-Dynamo", + stage=stage, + stack_name=stack_name ) self.s3_construct = S3Construct( self, - construct_id="ReservationApiS3", - stage=stage + construct_id=f"{stack_name}-S3", + stage=stage, + stack_name=stack_name ) self.ssm_construct = SsmConstruct( self, - construct_id="ReservationApiSsm", + construct_id=f"{stack_name}-Ssm", stage=stage, # atenção para esse próximo parâmetro. de preferencia deixe tudo minusculo sem _ # isso deve corresponder ao prefixo de caminho passado no CD dos outros mss (inclusive front) @@ -66,12 +58,11 @@ def __init__( ) ENVIRONMENT_VARIABLES = { - # o .upper() aqui existe por causa do enum em environments usado para o stage - "STAGE": stage.upper(), + "STAGE": stage, "DYNAMO_TABLE_NAME": self.dynamo_construct.table.table_name, "DYNAMO_PARTITION_KEY": "PK", "DYNAMO_SORT_KEY": "SK", - "REGION": self.aws_region, + "REGION": Aws.REGION, "USER_API_URL": os.environ.get("USER_API_URL"), "S3_BUCKET_NAME": self.s3_construct.bucket_spreadsheets.bucket_name, "FROM_EMAIL": os.environ.get("FROM_EMAIL"), @@ -81,8 +72,9 @@ def __init__( self.lambda_construct = LambdaConstruct( self, - construct_id="ReservationApiLambda", + construct_id=f"{stack_name}-Lambda", stage=stage, + stack_name=stack_name, api_gateway_resource=self.apigw_construct.api_gateway_resource, environment_variables=ENVIRONMENT_VARIABLES ) From d7098563c78530fe18940666e5da47080f2fede1 Mon Sep 17 00:00:00 2001 From: Leo Iorio Date: Tue, 31 Mar 2026 13:17:33 -0300 Subject: [PATCH 12/15] added remaining parameters to iac stack --- iac/app.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/iac/app.py b/iac/app.py index 6739bd3..063aef7 100644 --- a/iac/app.py +++ b/iac/app.py @@ -33,7 +33,9 @@ IacStack( app, - stack_id=stack_name, + stack_id=stack_name, + stack_name=stack_name, + stage=stage, env=cdk.Environment( account=aws_account_id, region=aws_region From a9e6b5dae89b483bd6d3956b320400249653ad73 Mon Sep 17 00:00:00 2001 From: Leo Iorio Date: Tue, 31 Mar 2026 13:25:06 -0300 Subject: [PATCH 13/15] reverting stack name to preserve stack logical id --- .github/workflows/CD.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index 7822c73..6241973 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -55,7 +55,7 @@ jobs: else echo "Invalid branch name!" && exit 1 fi - echo "STACK_NAME=ReservationApiStack" >> $GITHUB_ENV + echo "STACK_NAME=ReservationStackScheduleANDCourts${{env.STAGE}}" >> $GITHUB_ENV - name: Fetch config from SSM run: | From 00609f478b2c0ca29156d06a87249b558a662a26 Mon Sep 17 00:00:00 2001 From: Leo Iorio Date: Tue, 31 Mar 2026 13:30:44 -0300 Subject: [PATCH 14/15] limiting funciton name to 63 chars --- iac/components/lambda_construct.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/iac/components/lambda_construct.py b/iac/components/lambda_construct.py index 74a6563..3230480 100644 --- a/iac/components/lambda_construct.py +++ b/iac/components/lambda_construct.py @@ -26,7 +26,7 @@ def create_lambda_api_gateway_integration( function = lambda_.Function( self, id=module_name.title(), - function_name=f"{module_name}-{self.stack_name}-{self.stage}", + function_name=f"{module_name}-{self.stack_name}-{self.stage}"[:63], code=lambda_.Code.from_asset(f"../src/modules/{module_name}"), handler=f"app.{module_name}_presenter.lambda_handler", runtime=lambda_.Runtime("python3.13"), @@ -56,7 +56,7 @@ def create_lambda_event_bridge_integration( function = lambda_.Function( self, id=module_name.title(), - function_name=f"{module_name}-{self.stack_name}-{self.stage}", + function_name=f"{module_name}-{self.stack_name}-{self.stage}"[:63], code=lambda_.Code.from_asset(f"../src/modules/{module_name}"), handler=f"app.{module_name}_presenter.lambda_handler", runtime=lambda_.Runtime("python3.13"), From 7bd6cc0978512df4b052dc048568852fce08c1a5 Mon Sep 17 00:00:00 2001 From: Leo Iorio Date: Tue, 31 Mar 2026 13:40:43 -0300 Subject: [PATCH 15/15] changing stack name as logical ids are already messed up --- .github/workflows/CD.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index 6241973..fca481b 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -55,7 +55,7 @@ jobs: else echo "Invalid branch name!" && exit 1 fi - echo "STACK_NAME=ReservationStackScheduleANDCourts${{env.STAGE}}" >> $GITHUB_ENV + echo "STACK_NAME=ReservationStackApi" >> $GITHUB_ENV - name: Fetch config from SSM run: |