|
AWSTemplateFormatVersion: '2010-09-09' |
|
Description: AWS Fargate template |
|
|
|
Parameters: |
|
ImageUrl: |
|
Type: String |
|
Default: public.ecr.aws/d4y1q9n5/keycloak-only-intelligentrxcom:260400 |
|
KCHOSTNAME: |
|
Type: String |
|
Default: keycloak-only.intelligentrx.com |
|
KCDBUSERNAME: |
|
Type: String |
|
Default: KeycloakUsername |
|
KCDBPASSWORD: |
|
Type: String |
|
Default: KeycloakPassword |
|
NoEcho: true |
|
Environment: |
|
Type: String |
|
Default: dev |
|
Region: |
|
Type: String |
|
Default: us-east-1 |
|
DatabaseName: |
|
Type: String |
|
Default: KeycloakOnlyDatabase |
|
|
|
|
|
Mappings: |
|
# Hard values for the subnet masks. These masks define |
|
# the range of internal IP addresses that can be assigned. |
|
# The VPC can have all IP's from 10.0.0.0 to 10.0.255.255 |
|
# There are four subnets which cover the ranges: |
|
# |
|
# 10.0.0.0 - 10.0.63.255 (16384 IP addresses) |
|
# 10.0.64.0 - 10.0.127.255 (16384 IP addresses) |
|
# 10.0.128.0 - 10.0.191.255 (16384 IP addresses) |
|
# 10.0.192.0 - 10.0.255.0 (16384 IP addresses) |
|
SubnetConfig: |
|
VPC: |
|
CIDR: '10.30.0.0/16' |
|
PublicOne: |
|
CIDR: '10.30.0.0/18' |
|
PublicTwo: |
|
CIDR: '10.30.64.0/18' |
|
PrivateOne: |
|
CIDR: '10.30.128.0/18' |
|
PrivateTwo: |
|
CIDR: '10.30.192.0/18' |
|
PrivateBoth: |
|
CIDR: '10.30.128.0/17' |
|
|
|
Resources: |
|
VPC: |
|
Type: AWS::EC2::VPC |
|
Properties: |
|
EnableDnsSupport: true |
|
EnableDnsHostnames: true |
|
CidrBlock: !FindInMap ['SubnetConfig', 'VPC', 'CIDR'] |
|
|
|
# Two public subnets, where containers can have public IP addresses |
|
PublicSubnetOne: |
|
Type: AWS::EC2::Subnet |
|
Properties: |
|
AvailabilityZone: |
|
Fn::Select: |
|
- 0 |
|
- Fn::GetAZs: {Ref: 'AWS::Region'} |
|
VpcId: !Ref 'VPC' |
|
CidrBlock: !FindInMap ['SubnetConfig', 'PublicOne', 'CIDR'] |
|
MapPublicIpOnLaunch: true |
|
PublicSubnetTwo: |
|
Type: AWS::EC2::Subnet |
|
Properties: |
|
AvailabilityZone: |
|
Fn::Select: |
|
- 1 |
|
- Fn::GetAZs: {Ref: 'AWS::Region'} |
|
VpcId: !Ref 'VPC' |
|
CidrBlock: !FindInMap ['SubnetConfig', 'PublicTwo', 'CIDR'] |
|
MapPublicIpOnLaunch: true |
|
|
|
# Two private subnets where containers will only have private |
|
# IP addresses, and will only be reachable by other members of the |
|
# VPC |
|
PrivateSubnetOne: |
|
Type: AWS::EC2::Subnet |
|
Properties: |
|
AvailabilityZone: |
|
Fn::Select: |
|
- 0 |
|
- Fn::GetAZs: {Ref: 'AWS::Region'} |
|
VpcId: !Ref 'VPC' |
|
CidrBlock: !FindInMap ['SubnetConfig', 'PrivateOne', 'CIDR'] |
|
PrivateSubnetTwo: |
|
Type: AWS::EC2::Subnet |
|
Properties: |
|
AvailabilityZone: |
|
Fn::Select: |
|
- 1 |
|
- Fn::GetAZs: {Ref: 'AWS::Region'} |
|
VpcId: !Ref 'VPC' |
|
CidrBlock: !FindInMap ['SubnetConfig', 'PrivateTwo', 'CIDR'] |
|
|
|
# Setup networking resources for the public subnets. Containers |
|
# in the public subnets have public IP addresses and the routing table |
|
# sends network traffic via the internet gateway. |
|
InternetGateway: |
|
Type: AWS::EC2::InternetGateway |
|
GatewayAttachement: |
|
Type: AWS::EC2::VPCGatewayAttachment |
|
Properties: |
|
VpcId: !Ref 'VPC' |
|
InternetGatewayId: !Ref 'InternetGateway' |
|
PublicRouteTable: |
|
Type: AWS::EC2::RouteTable |
|
Properties: |
|
VpcId: !Ref 'VPC' |
|
PublicRoute: |
|
Type: AWS::EC2::Route |
|
DependsOn: GatewayAttachement |
|
Properties: |
|
RouteTableId: !Ref 'PublicRouteTable' |
|
DestinationCidrBlock: '0.0.0.0/0' |
|
GatewayId: !Ref 'InternetGateway' |
|
PublicSubnetOneRouteTableAssociation: |
|
Type: AWS::EC2::SubnetRouteTableAssociation |
|
Properties: |
|
SubnetId: !Ref PublicSubnetOne |
|
RouteTableId: !Ref PublicRouteTable |
|
PublicSubnetTwoRouteTableAssociation: |
|
Type: AWS::EC2::SubnetRouteTableAssociation |
|
Properties: |
|
SubnetId: !Ref PublicSubnetTwo |
|
RouteTableId: !Ref PublicRouteTable |
|
|
|
# Setup networking resources for the private subnets. Containers |
|
# in these subnets have only private IP addresses, and must use a NAT |
|
# gateway to talk to the internet. We launch two NAT gateways, one for |
|
# each private subnet. |
|
NATGatewayEIP: |
|
Type: AWS::EC2::EIP |
|
Properties: |
|
Domain: vpc |
|
NatGatewayOne: |
|
Type: AWS::EC2::NatGateway |
|
Properties: |
|
AllocationId: !GetAtt NATGatewayEIP.AllocationId |
|
SubnetId: !Ref PublicSubnetOne |
|
PrivateRouteTableOne: |
|
Type: AWS::EC2::RouteTable |
|
Properties: |
|
VpcId: !Ref 'VPC' |
|
PrivateRouteOne: |
|
Type: AWS::EC2::Route |
|
Properties: |
|
RouteTableId: !Ref PrivateRouteTableOne |
|
DestinationCidrBlock: 0.0.0.0/0 |
|
NatGatewayId: !Ref NatGatewayOne |
|
PrivateRouteTableOneAssociation: |
|
Type: AWS::EC2::SubnetRouteTableAssociation |
|
Properties: |
|
RouteTableId: !Ref PrivateRouteTableOne |
|
SubnetId: !Ref PrivateSubnetOne |
|
PrivateRouteTableTwo: |
|
Type: AWS::EC2::RouteTable |
|
Properties: |
|
VpcId: !Ref 'VPC' |
|
PrivateRouteTwo: |
|
Type: AWS::EC2::Route |
|
Properties: |
|
RouteTableId: !Ref PrivateRouteTableTwo |
|
DestinationCidrBlock: 0.0.0.0/0 |
|
# Note this was previously NatGatewayTwo |
|
NatGatewayId: !Ref NatGatewayOne |
|
PrivateRouteTableTwoAssociation: |
|
Type: AWS::EC2::SubnetRouteTableAssociation |
|
Properties: |
|
RouteTableId: !Ref PrivateRouteTableTwo |
|
SubnetId: !Ref PrivateSubnetTwo |
|
|
|
MainDBClusterProvisioned: |
|
Type: 'AWS::RDS::DBCluster' |
|
DeletionPolicy: Delete # This will prevent a final snapshot for the instance |
|
DependsOn: mainDBSubnetGroup |
|
Properties: |
|
MasterUsername: !Ref KCDBUSERNAME |
|
MasterUserPassword: !Ref KCDBPASSWORD |
|
BackupRetentionPeriod: 4 |
|
AutoMinorVersionUpgrade: true |
|
DatabaseName: !Ref DatabaseName |
|
DBClusterIdentifier: !Ref DatabaseName |
|
DBClusterParameterGroupName: default.aurora-postgresql15 |
|
DBSubnetGroupName: |
|
!Join [ |
|
"-", |
|
[!Ref Region, main-db-subnet-group, !Ref Environment], |
|
] |
|
EnableCloudwatchLogsExports: |
|
- postgresql |
|
EnableHttpEndpoint: False |
|
# aws rds describe-db-engine-versions --engine aurora-postgresql --query '*[].[EngineVersion]' --output text --region us-east-2 |
|
# aws rds describe-db-engine-versions --engine aurora-postgresql --query '*[].[EngineVersion]' --output text --region us-east-1 |
|
Engine: aurora-postgresql |
|
EngineVersion: 15.8 |
|
EngineMode: provisioned |
|
Port: 5432 |
|
SourceRegion: !Ref Region |
|
StorageEncrypted: true |
|
Tags: |
|
- Key: Name |
|
Value: !Join ["-", [!Ref Region, main, !Ref Environment]] |
|
VpcSecurityGroupIds: |
|
- !GetAtt MainDBSecurityGroup.GroupId |
|
|
|
MainDBSecurityGroup: |
|
Type: AWS::EC2::SecurityGroup |
|
Properties: |
|
GroupName: |
|
!Join [ |
|
"-", |
|
[!Ref Region, main-db-security-group-2, !Ref Environment], |
|
] |
|
GroupDescription: Allow inbound traffic |
|
SecurityGroupIngress: |
|
- IpProtocol: tcp |
|
FromPort: 5432 |
|
ToPort: 5432 |
|
CidrIp: !FindInMap ['SubnetConfig', 'PrivateBoth', 'CIDR'] |
|
SecurityGroupEgress: |
|
- IpProtocol: tcp |
|
FromPort: 5432 |
|
ToPort: 5432 |
|
CidrIp: !FindInMap ['SubnetConfig', 'PrivateBoth', 'CIDR'] |
|
VpcId: !Ref VPC |
|
|
|
RDSDBInstance1: |
|
Type: 'AWS::RDS::DBInstance' |
|
Properties: |
|
DBInstanceIdentifier: !Ref DatabaseName |
|
Engine: aurora-postgresql |
|
DBClusterIdentifier: !Ref MainDBClusterProvisioned |
|
PubliclyAccessible: false |
|
DBInstanceClass: db.t4g.medium |
|
|
|
mainDBSubnetGroup: |
|
Type: AWS::RDS::DBSubnetGroup |
|
Properties: |
|
DBSubnetGroupName: |
|
!Join [ |
|
"-", |
|
[!Ref Region, main-db-subnet-group, !Ref Environment], |
|
] |
|
DBSubnetGroupDescription: Used by MainDBCluster so that that DBCluster is inside the same VPC the host-dev or other service is inside |
|
SubnetIds: !Split |
|
- ',' |
|
- !Sub '${PrivateSubnetOne},${PrivateSubnetTwo}' |
|
Tags: |
|
- |
|
Key: Name |
|
Value: !Join ["-", [!Ref Region, main, !Ref Environment]] |
|
|
|
KeycloakOnlyRole: |
|
Type: AWS::IAM::Role |
|
Properties: |
|
RoleName: ecs-task-execution-role-KeycloakOnly |
|
AssumeRolePolicyDocument: |
|
Statement: |
|
- Effect: Allow |
|
Principal: |
|
Service: [ecs-tasks.amazonaws.com] |
|
Action: ["sts:AssumeRole"] |
|
Condition: |
|
ArnLike: |
|
aws:SourceArn: !Sub arn:aws:ecs:${AWS::Region}:${AWS::AccountId}:* |
|
StringEquals: |
|
aws:SourceAccount: !Ref AWS::AccountId |
|
Path: / |
|
|
|
ManagedPolicyArns: |
|
# This role enables basic features of ECS. See reference: |
|
# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/security-iam-awsmanpol.html#security-iam-awsmanpol-AmazonECSTaskExecutionRolePolicy |
|
- arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy |
|
- arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly |
|
# - arn:aws:iam::aws:policy/AmazonSSMFullAccess |
|
- arn:aws:iam::aws:policy/CloudWatchLogsFullAccess |
|
# This is the 'inline' permissions document |
|
Policies: |
|
- PolicyName: applicationspecific |
|
PolicyDocument: |
|
Version: "2012-10-17" |
|
Statement: |
|
- Effect: Allow |
|
Action: |
|
- kms:Decrypt |
|
- ssm:GetParameterHistory |
|
- ecr:GetDownloadUrlForLayer |
|
- ecr:GetAuthorizationToken |
|
- ssm:GetParameters |
|
- logs:PutLogEvents |
|
- ssm:GetParameter |
|
- logs:CreateLogStream |
|
- secretsmanager:GetSecretValue |
|
- kms:Encrypt |
|
- ecr:BatchGetImage |
|
- ssm:GetParametersByPath |
|
- ecr:BatchCheckLayerAvailability |
|
Resource: "*" |
|
|
|
# ECS Resources |
|
ECSCluster: |
|
Type: AWS::ECS::Cluster |
|
Properties: |
|
ClusterName: !Sub ecs-KeycloakOnly |
|
ClusterSettings: |
|
- Name: containerInsights |
|
Value: enabled |
|
|
|
TaskDefinition: |
|
Type: AWS::ECS::TaskDefinition |
|
Properties: |
|
Family: KeycloakOnly |
|
Cpu: 512 |
|
Memory: 2048 |
|
NetworkMode: awsvpc |
|
RequiresCompatibilities: |
|
- FARGATE |
|
ExecutionRoleArn: !GetAtt KeycloakOnlyRole.Arn |
|
TaskRoleArn: !GetAtt KeycloakOnlyRole.Arn |
|
ContainerDefinitions: |
|
- Name: KeycloakOnly |
|
User: nonprivuser |
|
Image: !Ref ImageUrl |
|
HealthCheck: |
|
Command: |
|
- "CMD-SHELL" |
|
- "{ printf 'HEAD /health HTTP/1.0\r\n\r\n' >&0; grep 'HTTP/1.0 200'; } 0<>/dev/tcp/localhost/9000 || exit 1" |
|
Interval: 30 |
|
Retries: 3 |
|
StartPeriod: 30 |
|
Timeout: 3 |
|
PortMappings: |
|
- ContainerPort: 9000 |
|
HostPort: 9000 |
|
- ContainerPort: 8443 |
|
HostPort: 8443 |
|
- ContainerPort: 7800 |
|
HostPort: 7800 |
|
Environment: |
|
- Name: "KC_HOSTNAME" |
|
Value: !Ref KCHOSTNAME |
|
- Name: "KC_DB_URL_HOST" |
|
Value: !GetAtt MainDBClusterProvisioned.Endpoint.Address |
|
- Name: "KC_DB_URL_PORT" |
|
Value: 5432 |
|
- Name: "KC_DB_URL_DATABASE" |
|
Value: postgres |
|
- Name: "KC_DB_USERNAME" |
|
Value: !Ref KCDBUSERNAME |
|
- Name: "KC_DB_PASSWORD" |
|
Value: !Ref KCDBPASSWORD |
|
- Name: "DB_KEYCLOAK_PASSWORD" |
|
Value: !Ref KCDBPASSWORD |
|
LogConfiguration: |
|
LogDriver: 'awslogs' |
|
Options: |
|
mode: non-blocking |
|
# As of 2025/05/20: |
|
# We have an extra 6+ GB of RAM and 19+ GiB of ephemeral storage in the main service to play with |
|
# Max logs per hour is 454 MB. Max daily is 5 GB |
|
# I have seen downtime as less than 1 hour in the past |
|
# https://app.clickup.com/t/86b4yet7r |
|
max-buffer-size: 4g |
|
awslogs-group: !Ref LogGroup |
|
awslogs-region: !Ref AWS::Region |
|
awslogs-stream-prefix: KeycloakOnly |
|
# Keycloak does NOT support a read-only file system |
|
# https://github.com/keycloak/keycloak/issues/11286 |
|
ReadonlyRootFilesystem: false |
|
|
|
# The service. The service is a resource which allows you to run multiple |
|
# copies of a type of task, and gather up their logs and metrics, as well |
|
# as monitor the number of running tasks and replace any that have crashed |
|
Service: |
|
Type: AWS::ECS::Service |
|
# Avoid race condition between ECS service creation and associating |
|
# the target group with the LB |
|
DependsOn: |
|
- PublicLoadBalancerListener |
|
- PublicLoadBalancerListenerRule1 |
|
- RDSDBInstance1 |
|
Properties: |
|
ServiceName: KeycloakOnly |
|
Cluster: !Ref ECSCluster |
|
LaunchType: FARGATE |
|
NetworkConfiguration: |
|
AwsvpcConfiguration: |
|
AssignPublicIp: DISABLED |
|
SecurityGroups: |
|
- !Ref ServiceSecurityGroup |
|
Subnets: !Split |
|
- ',' |
|
- !Sub '${PrivateSubnetOne},${PrivateSubnetTwo}' |
|
DeploymentConfiguration: |
|
MaximumPercent: 200 |
|
MinimumHealthyPercent: 75 |
|
# This is to speed up failed deployments - and yes, it does work |
|
DeploymentCircuitBreaker: |
|
Enable: Yes |
|
Rollback: Yes |
|
HealthCheckGracePeriodSeconds: 30 |
|
DeploymentController: |
|
Type: ECS |
|
DesiredCount: 1 |
|
TaskDefinition: !Ref TaskDefinition |
|
LoadBalancers: |
|
- ContainerName: KeycloakOnly |
|
ContainerPort: 8443 |
|
TargetGroupArn: !Ref ServiceTargetGroup |
|
|
|
# Security group that limits network access |
|
# to the task |
|
ServiceSecurityGroup: |
|
Type: AWS::EC2::SecurityGroup |
|
Properties: |
|
GroupDescription: Security group for service |
|
VpcId: !Ref VPC |
|
|
|
# Keeps track of the list of tasks for the service |
|
ServiceTargetGroup: |
|
Type: AWS::ElasticLoadBalancingV2::TargetGroup |
|
# It can take 2+ minutes to boot up for the very first time because "InstallRxNorm" takes more than one minute |
|
# With a 6 second * 10 timeout, there is insufficient time for a deployment to finish |
|
# I tried to rectify this by going to 15 * 8, or allowing 2 minutes to establish a target a healthy |
|
Properties: |
|
HealthCheckIntervalSeconds: 30 |
|
HealthCheckPath: /health |
|
HealthCheckPort: 9000 # moved in version 25 and (optionally) moved back in version 26.4.0 |
|
HealthCheckProtocol: HTTP |
|
HealthCheckTimeoutSeconds: 5 |
|
HealthyThresholdCount: 2 |
|
TargetType: ip |
|
Port: 8443 |
|
Protocol: HTTPS |
|
UnhealthyThresholdCount: 4 |
|
VpcId: !Ref VPC |
|
TargetGroupAttributes: |
|
- Key: deregistration_delay.timeout_seconds |
|
Value: 30 |
|
# Stickiness is so that if we ever DO have multiple instances it works correctly |
|
- Key: stickiness.enabled |
|
Value: true |
|
- Key: stickiness.type |
|
Value: lb_cookie |
|
|
|
# A public facing load balancer, this is used as ingress for |
|
# public facing internet traffic. |
|
PublicLoadBalancerSG: |
|
Type: AWS::EC2::SecurityGroup |
|
Properties: |
|
GroupDescription: Access to the public facing load balancer |
|
VpcId: !Ref VPC |
|
SecurityGroupIngress: |
|
# Allow access to public facing ALB from any IP address |
|
- CidrIp: 0.0.0.0/0 |
|
IpProtocol: tcp |
|
FromPort: 80 |
|
ToPort: 80 |
|
|
|
PublicLoadBalancer: |
|
Type: AWS::ElasticLoadBalancingV2::LoadBalancer |
|
Properties: |
|
Scheme: internet-facing |
|
LoadBalancerAttributes: |
|
- Key: idle_timeout.timeout_seconds |
|
Value: '30' |
|
Subnets: !Split |
|
- ',' |
|
- !Sub '${PublicSubnetOne},${PublicSubnetTwo}' |
|
SecurityGroups: |
|
- !Ref PublicLoadBalancerSG |
|
|
|
# This is the default rule; it is the fallback |
|
PublicLoadBalancerListener: |
|
Type: AWS::ElasticLoadBalancingV2::Listener |
|
Properties: |
|
DefaultActions: |
|
- Type: 'fixed-response' |
|
FixedResponseConfig: |
|
StatusCode: 404 |
|
LoadBalancerArn: !Ref 'PublicLoadBalancer' |
|
# In my template this is 443 w/ Protocol HTTP, but to use 443 and HTTPS you need a certificate & a domain which I don't want to include in this template |
|
Port: 80 |
|
Protocol: HTTP |
|
|
|
# this is the main rule we want to see used |
|
PublicLoadBalancerListenerRule1: |
|
Type: 'AWS::ElasticLoadBalancingV2::ListenerRule' |
|
Properties: |
|
Actions: |
|
- Type: forward |
|
TargetGroupArn: !Ref ServiceTargetGroup |
|
Conditions: |
|
- Field: path-pattern |
|
PathPatternConfig: |
|
Values: |
|
- "*" |
|
ListenerArn: !Ref PublicLoadBalancerListener |
|
Priority: 10 |
|
|
|
# Open up the service's security group to traffic originating |
|
# from the security group of the load balancer. |
|
ServiceIngressfromLoadBalancer: |
|
Type: AWS::EC2::SecurityGroupIngress |
|
Properties: |
|
Description: Ingress from the public ALB |
|
GroupId: !Ref ServiceSecurityGroup |
|
IpProtocol: tcp |
|
FromPort: 8443 |
|
ToPort: 8443 |
|
SourceSecurityGroupId: !Ref 'PublicLoadBalancerSG' |
|
|
|
ServiceIngressfromLoadBalancerKeycloak2: |
|
Type: AWS::EC2::SecurityGroupIngress |
|
Properties: |
|
Description: Ingress for task to task communication |
|
GroupId: !Ref ServiceSecurityGroup |
|
IpProtocol: tcp |
|
FromPort: 7800 |
|
ToPort: 7800 |
|
SourceSecurityGroupId: !Ref 'PublicLoadBalancerSG' |
|
|
|
ServiceIngressfromLoadBalancerKeycloakHealthCheck: |
|
Type: AWS::EC2::SecurityGroupIngress |
|
Properties: |
|
Description: Ingress for health check |
|
GroupId: !Ref ServiceSecurityGroup |
|
IpProtocol: tcp |
|
FromPort: 9000 |
|
ToPort: 9000 |
|
SourceSecurityGroupId: !Ref 'PublicLoadBalancerSG' |
|
|
|
# This log group stores the stdout logs from this service's containers |
|
LogGroup: |
|
Type: AWS::Logs::LogGroup |
|
Properties: |
|
LogGroupName: KeycloakOnly |
|
RetentionInDays: 2557 # 7 years (HIPAA) |
|
|
|
Outputs: |
|
HealthCheckProtocol: |
|
Value: HTTPS |