diff --git a/README.md b/README.md index 008619f..198a81f 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ infrastructure/ container-foundation.yaml container-application.yaml ec2-platform.yaml - native-cicd.yaml # planned + github-oidc.yaml policies/ security.guard ``` @@ -46,6 +46,8 @@ CloudFormation root stack. 6. `ec2-platform.yaml` creates the non-containerized ALB, private frontend and backend Auto Scaling groups, CodeDeploy resources, Systems Manager access, CloudWatch logs, scaling policies, and basic alarms. +7. `github-oidc.yaml` creates the credential-free GitHub Actions trust and + application deployment role for ECS and EC2 releases. The container platform is split into foundation and application stacks because the ECR repositories must exist before the first immutable images can be @@ -79,6 +81,13 @@ Application Load Balancer. Backend database credentials are not placed in CloudFormation user data or CodeDeploy bundles. The backend reads the RDS-managed secret at startup using its EC2 instance role. +The EC2 Auto Scaling groups use EC2 instance health rather than ELB health. +CodeDeploy intentionally removes instances from target groups during in-place +deployments; using ELB health at the Auto Scaling layer would replace healthy +instances while they are draining. Application health remains enforced by +CodeDeploy validation hooks, ALB target checks, CloudWatch alarms, and +automatic rollback. + ## Basic Monitoring Monitoring is embedded in the workload templates instead of being maintained @@ -139,11 +148,80 @@ application deployment workflow: - ECS cluster name - Frontend and backend ECS service names - Frontend and backend ECR repository URIs +- EC2 CodeDeploy artifact bucket, applications, and deployment groups The application repository builds and publishes immutable images. It does not run CloudFormation. This repository provisions and changes the platform. It does not build application source. +## GitHub OIDC Deployment Role + +Deploy `github-oidc.yaml` once per environment and configure its +`GitHubDeploymentRoleArn` output as the `AWS_ROLE_ARN` variable in the matching +GitHub Environment. Also configure `AWS_REGION`. + +```bash +aws cloudformation deploy \ + --stack-name coditude-dev-github-oidc \ + --template-file infrastructure/nested/github-oidc.yaml \ + --capabilities CAPABILITY_IAM \ + --parameter-overrides \ + ProjectName=coditude \ + Environment=dev \ + --profile coditude-dev \ + --region ap-south-1 +``` + +The role can publish ECR images, update existing ECS services, upload immutable +CodeDeploy artifacts under the EC2 platform bucket, and create deployments for +the environment's frontend and backend deployment groups. The application +workflows do not create EC2, networking, database, or load-balancer resources. + +## EC2 Deployment And Testing + +Create the EC2 platform through a reviewed CloudFormation change set. The +template creates one private frontend and backend Auto Scaling group, +CodeDeploy applications and deployment groups, an ALB, CloudWatch logs, +scaling, and alarms. + +After the stack completes, run **Deploy EC2** from the application repository. +Select `bootstrap=true` for the first release only. Later releases use +`bootstrap=false`. + +Inspect the platform: + +```bash +aws cloudformation describe-stacks \ + --stack-name coditude-dev-ec2-platform \ + --query 'Stacks[0].{Status:StackStatus,Outputs:Outputs}' \ + --output json \ + --profile coditude-dev \ + --region ap-south-1 + +aws autoscaling describe-auto-scaling-groups \ + --query 'AutoScalingGroups[?starts_with(AutoScalingGroupName, `coditude-dev-ec2-platform`)].{Name:AutoScalingGroupName,Desired:DesiredCapacity,Instances:Instances[].InstanceId}' \ + --output table \ + --profile coditude-dev \ + --region ap-south-1 +``` + +Verify the deployed application with the `ApplicationUrl` stack output: + +```bash +curl http://EC2_ALB_DNS/ +curl http://EC2_ALB_DNS/api/v1/message +``` + +The API response should contain: + +```json +{ + "message": "Backend is running successfully", + "environment": "dev", + "source": "postgresql" +} +``` + ## Multi-Environment Strategy One reusable template set supports `dev`, `staging`, and `prod`. The diff --git a/infrastructure/nested/ec2-platform.yaml b/infrastructure/nested/ec2-platform.yaml index 2047179..a02e9ae 100644 --- a/infrastructure/nested/ec2-platform.yaml +++ b/infrastructure/nested/ec2-platform.yaml @@ -298,7 +298,7 @@ Resources: Properties: HealthCheckEnabled: true HealthCheckIntervalSeconds: 30 - HealthCheckPath: / + HealthCheckPath: /api/health HealthCheckPort: traffic-port HealthCheckProtocol: HTTP HealthCheckTimeoutSeconds: 5 @@ -308,6 +308,9 @@ Resources: Port: 3000 Protocol: HTTP TargetType: instance + TargetGroupAttributes: + - Key: deregistration_delay.timeout_seconds + Value: "30" UnhealthyThresholdCount: 3 VpcId: !Ref VpcId Tags: @@ -331,6 +334,9 @@ Resources: Port: 8000 Protocol: HTTP TargetType: instance + TargetGroupAttributes: + - Key: deregistration_delay.timeout_seconds + Value: "30" UnhealthyThresholdCount: 3 VpcId: !Ref VpcId Tags: @@ -400,6 +406,36 @@ Resources: ListenerArn: !Ref HttpsListener Priority: 10 + PublicLoadBalancerToBackendEgress: + Type: AWS::EC2::SecurityGroupEgress + Properties: + Description: Forward API requests to the EC2 backend + DestinationSecurityGroupId: !Ref BackendSecurityGroupId + FromPort: 8000 + GroupId: !Ref PublicLoadBalancerSecurityGroupId + IpProtocol: tcp + ToPort: 8000 + + BackendFromPublicLoadBalancerIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + Description: Receive EC2 API requests from the public load balancer + FromPort: 8000 + GroupId: !Ref BackendSecurityGroupId + IpProtocol: tcp + SourceSecurityGroupId: !Ref PublicLoadBalancerSecurityGroupId + ToPort: 8000 + + FrontendInternetHttpEgress: + Type: AWS::EC2::SecurityGroupEgress + Properties: + CidrIp: 0.0.0.0/0 + Description: Call the public EC2 application endpoint through NAT + FromPort: 80 + GroupId: !Ref FrontendSecurityGroupId + IpProtocol: tcp + ToPort: 80 + FrontendLaunchTemplate: Type: AWS::EC2::LaunchTemplate Properties: @@ -421,6 +457,17 @@ Resources: HttpTokens: required SecurityGroupIds: - !Ref FrontendSecurityGroupId + TagSpecifications: + - ResourceType: instance + Tags: + - Key: Name + Value: !Sub "${ProjectName}-${Environment}-frontend" + - Key: Project + Value: coditude + - Key: Environment + Value: !Ref Environment + - Key: ManagedBy + Value: cloudformation UserData: Fn::Base64: !Sub | #!/bin/bash @@ -468,18 +515,6 @@ Resources: -a fetch-config -m ec2 \ -c file:/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json \ -s - TagSpecifications: - - ResourceType: instance - Tags: - - Key: Name - Value: !Sub "${ProjectName}-${Environment}-frontend" - - Key: Project - Value: coditude - - Key: Environment - Value: !Ref Environment - - Key: ManagedBy - Value: cloudformation - BackendLaunchTemplate: Type: AWS::EC2::LaunchTemplate Properties: @@ -501,6 +536,17 @@ Resources: HttpTokens: required SecurityGroupIds: - !Ref BackendSecurityGroupId + TagSpecifications: + - ResourceType: instance + Tags: + - Key: Name + Value: !Sub "${ProjectName}-${Environment}-backend" + - Key: Project + Value: coditude + - Key: Environment + Value: !Ref Environment + - Key: ManagedBy + Value: cloudformation UserData: Fn::Base64: !Sub | #!/bin/bash @@ -547,24 +593,12 @@ Resources: -a fetch-config -m ec2 \ -c file:/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json \ -s - TagSpecifications: - - ResourceType: instance - Tags: - - Key: Name - Value: !Sub "${ProjectName}-${Environment}-backend" - - Key: Project - Value: coditude - - Key: Environment - Value: !Ref Environment - - Key: ManagedBy - Value: cloudformation - FrontendAutoScalingGroup: Type: AWS::AutoScaling::AutoScalingGroup Properties: DesiredCapacity: !Ref FrontendDesiredCapacity HealthCheckGracePeriod: 300 - HealthCheckType: ELB + HealthCheckType: EC2 LaunchTemplate: LaunchTemplateId: !Ref FrontendLaunchTemplate Version: !GetAtt FrontendLaunchTemplate.LatestVersionNumber @@ -589,7 +623,7 @@ Resources: Properties: DesiredCapacity: !Ref BackendDesiredCapacity HealthCheckGracePeriod: 300 - HealthCheckType: ELB + HealthCheckType: EC2 LaunchTemplate: LaunchTemplateId: !Ref BackendLaunchTemplate Version: !GetAtt BackendLaunchTemplate.LatestVersionNumber diff --git a/infrastructure/nested/github-oidc.yaml b/infrastructure/nested/github-oidc.yaml index 67dad22..899cf88 100644 --- a/infrastructure/nested/github-oidc.yaml +++ b/infrastructure/nested/github-oidc.yaml @@ -172,6 +172,28 @@ Resources: - servicediscovery:TagResource - servicediscovery:UntagResource Resource: "*" + - PolicyName: DeployEc2Application + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - s3:GetObject + - s3:PutObject + Resource: !Sub + - "arn:${AWS::Partition}:s3:::${Project}-${Stage}-ec2-platform-deploymentartifactbucket-*/releases/*" + - Project: !Ref ProjectName + Stage: !Ref Environment + - Effect: Allow + Action: + - codedeploy:CreateDeployment + - codedeploy:GetDeployment + - codedeploy:GetDeploymentGroup + - codedeploy:UpdateDeploymentGroup + Resource: !Sub + - "arn:${AWS::Partition}:codedeploy:${AWS::Region}:${AWS::AccountId}:deploymentgroup:${Project}-${Stage}-*/${Project}-${Stage}-*" + - Project: !Ref ProjectName + Stage: !Ref Environment Tags: - Key: Project Value: coditude @@ -182,7 +204,7 @@ Resources: Outputs: GitHubDeploymentRoleArn: - Description: Role ARN configured in the GitHub dev environment. + Description: Application deployment role configured in the GitHub environment. Value: !GetAtt GitHubDeploymentRole.Arn GitHubOidcProviderArn: