Skip to content

Instantly share code, notes, and snippets.

@thomasdarimont
Forked from Gregory-Ledray/README.md
Created October 9, 2025 23:28
Show Gist options
  • Save thomasdarimont/d7ce94da9fb6cc296d274e032ec2a879 to your computer and use it in GitHub Desktop.
Save thomasdarimont/d7ce94da9fb6cc296d274e032ec2a879 to your computer and use it in GitHub Desktop.
Keycloak 26.4.0 on AWS with Fargate and Aurora Postgres

README

This gist is for reproducing keycloak/keycloak#43194

I built the Dockerfile with 3 sets of parameters, matching 26.2.3 with tag 260203, 26.3.5 with tag 260305, and 26.4.0 with tag 260400. Control which Dockerfile tag is used in CloudFormation with parameter overrides, like --parameter-overrides ImageUrl=public.ecr.aws/d4y1q9n5/keycloak-only-intelligentrxcom:260203

Steps for deploying each CloudFormation template are below. I did:

  1. Deployed 26.2.3 (worked)
  2. Updated by deploying with 26.3.5 (failed, and had to fix keycloak-only.yml)
  3. Updated by deploying with 26.2.5 aws cloudformation deploy --template-file ./keycloak-only.yml --stack-name KeycloakOnly --capabilities CAPABILITY_NAMED_IAM --parameter-overrides ImageUrl=public.ecr.aws/d4y1q9n5/keycloak-only-intelligentrxcom:260205 (success)
  4. Updated by deploying with 26.3.5 aws cloudformation deploy --template-file ./keycloak-only.yml --stack-name KeycloakOnly --capabilities CAPABILITY_NAMED_IAM --parameter-overrides ImageUrl=public.ecr.aws/d4y1q9n5/keycloak-only-intelligentrxcom:260305 (worked)
  5. Updated by deploying with 26.4.0 aws cloudformation deploy --template-file ./keycloak-only.yml --stack-name KeycloakOnly --capabilities CAPABILITY_NAMED_IAM --parameter-overrides ImageUrl=public.ecr.aws/d4y1q9n5/keycloak-only-intelligentrxcom:260400 (worked)

Deploy 26.4.0

  1. Perform steps in Build if you need to build the Dockerfile (should not be necessary)
  2. If not done already, Authenticate. aws sso login
  3. Run:
aws cloudformation deploy --template-file ./keycloak-only.yml --stack-name KeycloakOnly --capabilities CAPABILITY_NAMED_IAM --parameter-overrides ImageUrl=public.ecr.aws/d4y1q9n5/keycloak-only-intelligentrxcom:260400

Deploy Other Versions

Replace 260400 with the appropriate version, like 260305 or 260205 or 260203

Build All Versions

docker build -t public.ecr.aws/d4y1q9n5/keycloak-only-intelligentrxcom:260203 -f keycloak-only.dockerfile --build-arg KEYCLOAK_VERSION="26.2.3" . && docker push public.ecr.aws/d4y1q9n5/keycloak-only-intelligentrxcom:260203 && docker build -t public.ecr.aws/d4y1q9n5/keycloak-only-intelligentrxcom:260205 -f keycloak-only.dockerfile --build-arg KEYCLOAK_VERSION="26.2.5" . && docker push public.ecr.aws/d4y1q9n5/keycloak-only-intelligentrxcom:260205 && docker build -t public.ecr.aws/d4y1q9n5/keycloak-only-intelligentrxcom:260305 -f keycloak-only.dockerfile --build-arg KEYCLOAK_VERSION="26.3.5" . && docker push public.ecr.aws/d4y1q9n5/keycloak-only-intelligentrxcom:260305 && docker build -t public.ecr.aws/d4y1q9n5/keycloak-only-intelligentrxcom:260400 -f keycloak-only.dockerfile --build-arg KEYCLOAK_VERSION="26.4.0" . && docker push public.ecr.aws/d4y1q9n5/keycloak-only-intelligentrxcom:260400

Log for creating Keycloak-Only

  • Removed hosted zones & public certificates & moved from port 443 & HTTPS on load balancer to HTTP. This makes it easier to deploy. Removed Route53
  • aws ecr-public create-repository --repository-name keycloak-only-intelligentrxcom --region us-east-1 and put output URL as default value for ImageUrl parameter
  • I expect this to be non-accessible because keycloak-only.intelligentrx.com isn't going to be mapped to the load balancer. That is OK because the service is supposed to fail on startup & I do not anticipate any traffic between Keycloak instances going over the public internet / needing DNS resolution as a result
  • Removed KMS keys whenever possible
ARG KEYCLOAK_VERSION=26.4.0
FROM registry.access.redhat.com/ubi9 AS ubi-micro-build
ARG KEYCLOAK_VERSION=26.4.0
ARG KC_HOSTNAME=keycloak-only.intelligentrx.com
ARG KEYCLOAK_DIST=https://github.com/keycloak/keycloak/releases/download/${KEYCLOAK_VERSION}/keycloak-${KEYCLOAK_VERSION}.tar.gz
RUN dnf install -y tar gzip java-17-openjdk-devel maven-openjdk17
ADD $KEYCLOAK_DIST /tmp/keycloak/
RUN (cd /tmp/keycloak && \
tar -xvf /tmp/keycloak/keycloak-*.tar.gz && \
rm /tmp/keycloak/keycloak-*.tar.gz) || true
RUN mv /tmp/keycloak/keycloak-* /opt/keycloak && mkdir -p /opt/keycloak/data
RUN chmod -R g+rwX /opt/keycloak
COPY ubi-null.sh /tmp/
RUN bash /tmp/ubi-null.sh java-17-openjdk-headless glibc-langpack-en
WORKDIR /
FROM registry.access.redhat.com/ubi9-micro AS keycloak-image
ENV LANG=en_US.UTF-8
COPY --from=ubi-micro-build /tmp/null/rootfs/ /
COPY --from=ubi-micro-build --chown=1000:0 /opt/keycloak /opt/keycloak
RUN echo "keycloak:x:0:root" >> /etc/group && \
echo "keycloak:x:1000:0:keycloak user:/opt/keycloak:/sbin/nologin" >> /etc/passwd
USER 1000
EXPOSE 7800
EXPOSE 8080
EXPOSE 8443
EXPOSE 9000
ENTRYPOINT [ "/opt/keycloak/bin/kc.sh" ]
FROM keycloak-image AS builder
# Configure build options
ENV KC_DB=postgres
# feature (explanation for enabling or disabling). Documents for features: https://www.keycloak.org/server/features
# admin2 (must be able to view a console so I can edit users in an emergency), admin-api (needed by admin2), web-authn (for passwordless login), admin-api (), account-api and account3 because of
ENV KC_FEATURES=admin:v2,admin-api,web-authn,account-api,account:v3,hostname:v2,transient-users
# admin-fine-grained-authz (experimental -> risky), admin (deprecated), docker (don't need), impersonation (makes auditing too hard), scripts (don't need), token-exchange (don't need), ciba (?), par (don't need?),dynamic-scopes (don't need?), client-secret-rotation (don't need?), step-up-authentication (don't need?), recovery-codes (don't need?), update-email (don't need?), js-adapter (don't need?), device-flow (apple tv apps, don't need), dpop (preview; OAuth 2.0 Demonstrating Proof-of-Possession at the Application Layer), fips (FIPS 140-2 mode), login2 (??), multi-site (multi-site support), transient-users (if using external identity provider, this lets you not create the user in Keycloak), client-types (https://github.com/keycloak/keycloak/discussions/8497), kerberos (an auth method; don't need), oid4vc-vci (don't need), declarative-ui (experimental)
ENV KC_FEATURES_DISABLED=admin-fine-grained-authz,docker,impersonation,scripts,token-exchange,ciba,par,dynamic-scopes,client-secret-rotation,step-up-authentication,recovery-codes,update-email,authorization,client-policies,device-flow,dpop,fips,multi-site,client-types,kerberos,oid4vc-vci,declarative-ui
# deprecated linkedin-oauth (don't need), account2, offline-session-preloading
# preview is for all preview features
ENV KC_HEALTH_ENABLED=true
ENV KC_METRICS_ENABLED=true
WORKDIR /opt/keycloak
# Don't need to use a proper certificate because only ALB can access this ECS instance
RUN keytool -genkeypair -storepass password -storetype PKCS12 -keyalg RSA -keysize 2048 -dname "CN=server" -alias server -ext "SAN:c=DNS:localhost,IP:127.0.0.1" -keystore conf/server.keystore
RUN /opt/keycloak/bin/kc.sh build
FROM quay.io/keycloak/keycloak:${KEYCLOAK_VERSION} AS f
COPY --from=builder /opt/keycloak/ /opt/keycloak/
ENV KC_DB_SCHEMA=public
ENV KC_DB_URL_DATABASE=postgres
# WARN [org.keycloak.quarkus.runtime.cli.Picocli] (main) The following build time non-cli options were found, but will be ignored during run time: kc.db, kc.features, kc.features-disabled, kc.health-enabled, kc.metrics-enabled
# So I REMOVED ENV KC_FEATURES and KC_FEATURES_DISABLED
# Hostname settings
ENV KC_HOSTNAME=keycloak-only.intelligentrx.com
# ENV KC_HOSTNAME_ADMIN=
# KC_HOSTNAME_STRICT=false and KC_HOSTNAME_STRICT_BACKCHANNEL=false because the certificate is attached to the ALB, not this instance.
# It does use HTTPS between ALB and Keycloak, but the certificate is made up
ENV KC_HOSTNAME_STRICT=false
ENV KC_HOSTNAME_BACKCHANNEL_DYNAMIC=false
# HTTP/TLS settings
ENV KC_HTTP_ENABLED=false
ENV KC_HTTP_PORT=80
ENV KC_HTTPS_PROTOCOLS=TLSv1.3,TLSv1.2
# WARN [org.keycloak.quarkus.runtime.cli.Picocli] (main) The following build time non-cli options were found, but will be ignored during run time: kc.db, kc.features, kc.features-disabled, kc.health-enabled, kc.metrics-enabled
ENV KC_HEALTH_ENABLED=true
# https://www.keycloak.org/server/reverseproxy
# We encrypt to load balancer, and then terminate, and then reencrypt to Keycloak
# The suggested migration is KC_PROXY_HEADERS=forwarded|xforwarded
# https://www.keycloak.org/docs/latest/upgrading/index.html#deprecated-proxy-option
ENV KC_PROXY_HEADERS=forwarded
# Logging Configuration
ENV KC_LOG=console
ENV KC_LOG_CONSOLE_COLOR=false
ENV KC_LOG_CONSOLE_OUTPUT=default
ENV KC_LOG_LEVEL=DEBUG
ENV KEYCLOAK_ADMIN=admin
ENV KEYCLOAK_ADMIN_PASSWORD=password
# works:
# HEALTHCHECK --interval=60s --timeout=5s --start-period=30s --retries=3 CMD curl --fail --insecure https://localhost:9000 || exit 1
# causes health check to fail:
# HEALTHCHECK --interval=60s --timeout=5s --start-period=30s --retries=3 CMD { printf 'HEAD /health HTTP/1.0\r\n\r\n' >&0; grep 'HTTP/1.0 200'; } 0<>/dev/tcp/localhost/9000 || exit 1
# in testing this worked on localhost:
ENV KC_HTTP_MANAGEMENT_SCHEME=http
HEALTHCHECK --interval=60s --timeout=5s --start-period=30s --retries=3 CMD { printf 'HEAD /health HTTP/1.0\r\n\r\n' >&0; grep 'HTTP/1.0 200'; } 0<>/dev/tcp/localhost/9000 || exit 1
EXPOSE 7800
EXPOSE 8443
EXPOSE 9000
# Create non-elevated user
USER 0
RUN echo "nonpriv:x:10001" >> /etc/group
RUN echo "nonprivuser:x:10001:10001:nonprivuser user:/opt/keycloak:/sbin/nologin" >> /etc/passwd
RUN chown -cR nonprivuser:nonpriv /opt/keycloak
USER nonprivuser
ENTRYPOINT ["/opt/keycloak/bin/kc.sh", "start", "--optimized", "--log-level=DEBUG"]
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
#!/bin/bash
# tr -d '\r' < ubi-null.sh > temp.sh && mv temp.sh ubi-null.sh
set -euo pipefail
#set -x
dir="/tmp/null"
rm -rf "$dir"
mkdir "$dir"
cd "$dir"
# Add all arguments as the initial core packages
printf '%s\n' "$@" > keep
# Packages required for a shell environment
cat >>keep <<EOF
bash
coreutils-single
EOF
# Disallow list to block certain packages and their dependencies
cat >disallow <<EOF
alsa-lib
copy-jdk-configs
cups-libs
chkconfig
info
gawk
platform-python
platform-python-setuptools
python3
python3-libs
python3-pip-wheel
python3-setuptools-wheel
p11-kit
EOF
sort -u keep -o keep
echo "==> Installing packages into chroot" >&2
set -x
# Install requirements for this script (xargs and cmp)
dnf install -y findutils diffutils
# Install core packages to chroot
rootfs="$(realpath rootfs)"
mkdir -p "$rootfs"
<keep xargs dnf install -y --installroot "$rootfs" --releasever 9 --setopt install_weak_deps=false --nodocs
dnf --installroot "$rootfs" clean all
rm -rf "$rootfs"/var/cache/* "$rootfs"/var/log/dnf* "$rootfs"/var/log/yum.*
{ set +x; } 2>/dev/null
echo "==> Building dependency tree" >&2
# Loop until we have the full dependency tree (no new packages found)
touch old
while ! cmp -s keep old
do
# 1. Get requirement names (not quite the same as package names)
# 2. Filter out any install-time requirements
# 3. Query which packages are being used to satisfy the requirements
# 4. Keep just their package names
# 5. Remove packages that are on the disallow list
# 6. Store result as an allowlist
<keep xargs rpm -r "$rootfs" -q --requires | sort -Vu | cut -d ' ' -f1 \
| grep -v -e '^rpmlib(' \
| xargs -d $'\n' rpm -r "$rootfs" -q --whatprovides \
| grep -v -e '^no package provides' \
| sed -r 's/^(.*)-.*-.*$/\1/' \
| grep -vxF -f disallow \
> new || true
# Safely replace the keep list, appending the new names
mv keep old
cat old new > keep
# Sort and deduplicate so cmp will eventually return true
sort -u keep -o keep
done
# Determine all packages that need to be removed
rpm -r "$rootfs" -qa | sed -r 's/^(.*)-.*-.*$/\1/' | sort -u > all
# Set complement (all - keep)
grep -vxF -f keep all > remove
echo "==> $(wc -l remove | cut -d ' ' -f1) packages to erase:" >&2
cat remove
echo "==> $(wc -l keep | cut -d ' ' -f1) packages to keep:" >&2
cat keep
echo "" >&2
echo "==> Erasing packages" >&2
# Delete all packages that aren't needed for the core packages
set -x
<remove xargs rpm -r "$rootfs" --erase --nodeps --allmatches
{ set +x; } 2>/dev/null
echo "" >&2
echo "==> Packages erased ok!" >&2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment