Last active
August 24, 2021 02:53
-
-
Save RikudouSage/60ade9ec2e24ebd408951e8854973518 to your computer and use it in GitHub Desktop.
serverless.yml complex example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
service: invoice-api # the service name, used as a prefix for all resources | |
package: # the packaging config | |
individually: true # this means that every function will be packaged separately and can define its own inclusion/exclusion rules | |
exclude: | |
- .idea/** | |
- var/cache/dev/** | |
- var/dev.log | |
- .gitlab-ci.yml | |
- .php_cs.dist | |
- .phpstorm.meta.php | |
- composer.json | |
- composer.lock | |
- phpstan.neon | |
- rector.php | |
- symfony.lock | |
- pdf-server/**/* | |
custom: # you can put anything in here | |
# I use these values later in the code and they're separated by the environment which allows me to switch the parameters just by changing | |
# provider.stage | |
cache_dir: | |
dev: '/tmp/app/cache' | |
prod: '%kernel.project_dir%/var/cache' | |
log_dir: | |
dev: '/tmp/app/log' | |
prod: '/tmp/app/log' | |
provider: | |
name: aws | |
region: eu-central-1 | |
runtime: provided | |
stage: prod # by changing this I can deploy multiple versions of this app and I also fetch the custom paramters by this value | |
iamRoleStatements: # These statements allow you to set permissions for the function to act on other resources | |
- Effect: Allow | |
Resource: !GetAtt CacheTable.Arn # The !GetAtt is a function that allows getting attributes from resources, in this case arn | |
# which is basically the unique ID of every resource on AWS | |
Action: | |
- dynamodb:DescribeTable | |
- dynamodb:Query | |
- dynamodb:Scan | |
- dynamodb:GetItem | |
- dynamodb:PutItem | |
- dynamodb:UpdateItem | |
- dynamodb:DeleteItem | |
- Effect: Allow | |
Resource: | |
# !Join is another function for joining strings together, since I need this to work on all objects of the bucket | |
# I create the arn string by joining the resource arn and * with / | |
- !Join ['/', [!GetAtt UploadedInvoicesBucket.Arn, '*']] | |
- !GetAtt UploadedInvoicesBucket.Arn | |
Action: | |
- s3:PutObject | |
- s3:GetObject | |
- s3:ListBucket | |
- s3:DeleteObject | |
- Effect: Allow | |
Resource: | |
# RDS databases sadly don't return its arn in CloudFormation so I had to construct it manually while using some variables | |
# automatically provided by AWS | |
- !Sub "arn:${AWS::Partition}:rds:${AWS::Region}:${AWS::AccountId}:cluster:${AuroraRdsCluster}" | |
Action: | |
- rds:DescribeDBClusters | |
- rds:ModifyCurrentDBClusterCapacity | |
apiGateway: | |
# set this if your app is not an api but a web application, the api gateway then treats all responses as binary types | |
binaryMediaTypes: | |
- '*/*' | |
stackTags: | |
# These tags will be added to every resource, these three I use I also have set as billing tags which means I can | |
# track the cost of all apps I use. Beware that adding tags after the resources were already created may not work | |
# for all resource types. | |
ClientName: 'My Name' | |
BillingProject: Invoice | |
BillingSubproject: Invoice-${self:provider.stage} | |
environment: | |
BREF_BINARY_RESPONSES: 1 # This is important if you want to run a non-api app, should be used in combination with binaryMediaTypes | |
APP_SECRET: ${env:REMOTE_APP_SECRET} # Take an env variable, I deploy this app using CI which contains the env variable REMOTE_APP_SECRET | |
# Here again I use !Join to create the database url variable using a combination of env variables from CI and attributes from resources | |
DATABASE_URL: !Join ['', ['mysql://root:', '${env:REMOTE_MYSQL_PASSWORD}', '@', !GetAtt AuroraRdsCluster.Endpoint.Address, ':3306/invoice_api']] | |
# Here I set the APP_ENV the same as the stage name which means I can easily deploy dev/prod version changing just one line | |
APP_ENV: ${self:provider.stage} | |
# As mentioned before, the cache and logs dir is set using the custom values based on the environment | |
CACHE_DIR: ${self:custom.cache_dir.${self:provider.stage}} | |
LOG_DIR: ${self:custom.log_dir.${self:provider.stage}} | |
JWT_PASSPHRASE: ${env:REMOTE_JWT_SECRET} | |
JWT_SECRET_KEY: '%kernel.project_dir%/config/jwt/private.prod.pem' | |
JWT_PUBLIC_KEY: '%kernel.project_dir%/config/jwt/public.prod.pem' | |
# !Ref is similar to !GetAtt, for DynamoDB it returns the table name, you need to read the documenation to know which one to use | |
# based on what you need | |
DYNAMO_DB_TABLE: !Ref CacheTable | |
SSO_URL_INTERNAL: '' # I deleted the values for this example because they're not needed and would leak information that's irrelevant | |
SSO_URL_PUBLIC: '' | |
SSO_REDIRECT_URI: '' | |
SSO_CLIENT: '' | |
SSO_SECRET: ${env:REMOTE_SSO_SECRET} | |
plugins: | |
- ./vendor/bref/bref | |
- serverless-iam-roles-per-function # This is a serverless plugin that allows you to define individual access roles for each function | |
functions: | |
# I created this function before I created a NAT instance to have access to internet (more on that below) | |
# The code is part of the main app but allows access without login and because it's not in VPC has also access | |
# to outgoint internet, it's one of possible solutions to have internet in VPC if you don't want to use NAT instance/gateway | |
ares: | |
handler: public/index.php | |
timeout: 28 | |
environment: | |
APP_ENV: ares | |
APP_DEBUG: false | |
layers: | |
- ${bref:layer.php-74-fpm} | |
events: | |
- http: 'ANY /ares-data' | |
- http: 'ANY /ares-data/{proxy+}' | |
# This is a totally separate app that's written in javascript, I could write it in a separate project but I wanted it | |
# to be accessible on the same url | |
pdf-server: | |
runtime: nodejs12.x | |
environment: | |
FONTCONFIG_PATH: /opt/etc/fonts | |
package: | |
exclude: | |
- '**/*' | |
include: | |
- pdf-server/**/* | |
handler: pdf-server/handler.handle | |
events: | |
- http: 'ANY /pdf-generator' | |
- http: 'ANY /pdf-generator/{proxy+}' | |
layers: | |
- arn:aws:lambda:eu-central-1:347599033421:layer:wkhtmltopdf-0_12_6:1 | |
- arn:aws:lambda:eu-central-1:347599033421:layer:amazon_linux_fonts:1 | |
# This is the main website | |
website: | |
handler: public/index.php | |
vpc: # This is a VPC configuration which means the function is in a custom VPC, not the default one | |
securityGroupIds: # Every VPC has a default security group which allows all access from within the VPC | |
- !GetAtt ServerlessVPC.DefaultSecurityGroup | |
subnetIds: # Here you need to assign the subnets, just one is needed but at least one per zone is recommended (you don't pay for those) | |
- Ref: PrivateSubnetA | |
- Ref: PrivateSubnetB | |
- Ref: PrivateSubnetC | |
timeout: 28 | |
layers: | |
- ${bref:layer.php-74-fpm} | |
events: | |
- http: 'ANY /' | |
- http: 'ANY /{proxy+}' | |
# The Symfony console layer which allows me to run CLI commands | |
console: | |
handler: bin/console | |
vpc: | |
securityGroupIds: | |
- !GetAtt ServerlessVPC.DefaultSecurityGroup | |
subnetIds: | |
- Ref: PrivateSubnetA | |
- Ref: PrivateSubnetB | |
- Ref: PrivateSubnetC | |
timeout: 120 | |
layers: | |
- ${bref:layer.php-74} | |
- ${bref:layer.console} | |
resources: | |
Resources: | |
# Here I define the VPC this whole app resides in. | |
# The VPC doesn't cost you anything. | |
# The whole reason why I use VPC is because Lambda doesn't have access to your internal AWS resources (those that don't have public access) | |
# unless it's inside VPC. | |
# I also create a NAT instance that routes traffic from Lambda to internet (some of the AWS resources and also general outside connection). | |
# By default you can have only one: Lambda has access to internet (no VPC) or Lambda has access to internal resources (VPC). | |
ServerlessVPC: | |
Type: AWS::EC2::VPC | |
Properties: | |
CidrBlock: 10.1.0.0/16 # Any IP address block, read more about CIDR format online to create your own | |
EnableDnsSupport: true | |
EnableDnsHostnames: true | |
InstanceTenancy: default | |
Tags: | |
- Key: Name | |
Value: InvoiceApiVPC | |
# Here I create the private subnets for the VPC, those are accessible only from within the VPC and not from outside. | |
# The subnets don't cost you anything. | |
PrivateSubnetA: | |
Type: AWS::EC2::Subnet | |
Properties: | |
VpcId: | |
Ref: ServerlessVPC | |
# There are multiple availability zones in each region, I use the eu-central-1 region, which has zones a, b and c | |
# Here I'm creating it in eu-central-1a, each subnet should be in different zone | |
AvailabilityZone: ${self:provider.region}a | |
# Each subnet also needs its own CIDR block, again read more on the internet | |
CidrBlock: 10.1.0.0/24 | |
Tags: | |
- Key: Name | |
Value: InvoiceApiPrivateSubnetA | |
PrivateSubnetB: | |
Type: AWS::EC2::Subnet | |
Properties: | |
VpcId: | |
Ref: ServerlessVPC | |
# As mentioned above, here I create a subnet in a different availability zone, this time eu-central-1b | |
AvailabilityZone: ${self:provider.region}b | |
CidrBlock: 10.1.1.0/24 | |
Tags: | |
- Key: Name | |
Value: InvoiceApiPrivateSubnetB | |
PrivateSubnetC: | |
Type: AWS::EC2::Subnet | |
Properties: | |
VpcId: | |
Ref: ServerlessVPC | |
AvailabilityZone: ${self:provider.region}c | |
CidrBlock: 10.1.2.0/24 | |
Tags: | |
- Key: Name | |
Value: InvoiceApiPrivateSubnetC | |
# Because I need some connection from the functions to the internet, I need to also have a public subnet. | |
# For best availability there should as well be one in each availability zone, but I decided to create only one because I was lazy | |
# and the app is only for personal use so I don't really need the availability. | |
PublicSubnetA: | |
Type: AWS::EC2::Subnet | |
Properties: | |
VpcId: !Ref ServerlessVPC | |
AvailabilityZone: ${self:provider.region}a | |
CidrBlock: 10.1.3.0/24 | |
Tags: | |
- Key: Name | |
Value: InvoiceApiPublicSubnetA | |
# The VPC needs route tables if you want to control how your traffic behaves. | |
# Default one is created when you create the VPC but creating your own gives you more control over how the traffic is handled. | |
PrivateRouteTable: | |
Type: AWS::EC2::RouteTable | |
Properties: | |
VpcId: !Ref ServerlessVPC | |
Tags: | |
- Key: Name | |
Value: InvoiceApiPrivateRouteTable | |
# The same as above except this one is used for access outside of the VPC with the NAT instance, it needs to be separate | |
# from the private one. | |
PublicRouteTable: | |
Type: AWS::EC2::RouteTable | |
Properties: | |
VpcId: !Ref ServerlessVPC | |
Tags: | |
- Key: Name | |
Value: InvoiceApiPublicRouteTable | |
# Here I just associate the route tables with the correct subnets, the private route table is assigned to all privates subnets | |
# and the public is associated with public subnet. | |
PrivateRouteTableAssociationA: | |
Type: AWS::EC2::SubnetRouteTableAssociation | |
Properties: | |
SubnetId: !Ref PrivateSubnetA | |
RouteTableId: !Ref PrivateRouteTable | |
PrivateRouteTableAssociationB: | |
Type: AWS::EC2::SubnetRouteTableAssociation | |
Properties: | |
SubnetId: !Ref PrivateSubnetB | |
RouteTableId: !Ref PrivateRouteTable | |
PrivateRouteTableAssociationC: | |
Type: AWS::EC2::SubnetRouteTableAssociation | |
Properties: | |
SubnetId: !Ref PrivateSubnetC | |
RouteTableId: !Ref PrivateRouteTable | |
PublicRouteTableAssociationA: | |
Type: AWS::EC2::SubnetRouteTableAssociation | |
Properties: | |
SubnetId: !Ref PublicSubnetA | |
RouteTableId: !Ref PublicRouteTable | |
# To access the internet you not only need some NAT instance/gateway but also an internet gateway, it doesn't cost anything | |
InternetGateway: | |
Type: AWS::EC2::InternetGateway | |
Properties: | |
Tags: | |
- Key: Name | |
Value: InvoiceApiInternetGateway | |
# You need to create an association between the internet gateway and the VPC | |
InternetGatewayAttachment: | |
Type: AWS::EC2::VPCGatewayAttachment | |
Properties: | |
InternetGatewayId: !Ref InternetGateway | |
VpcId: !Ref ServerlessVPC | |
# Here I assign that all outgoing traffic (0.0.0.0/0 means all) should be routed to NatEc2 (created below) and associated with | |
# the private route table. This basically means that anything that uses the private subnets and tries to reach anywhere outside the VPC | |
# will go through the NAT instance. | |
PrivateRouteTableNatRoute: | |
Type: AWS::EC2::Route | |
Properties: | |
DestinationCidrBlock: 0.0.0.0/0 | |
InstanceId: !Ref NatEc2 | |
RouteTableId: !Ref PrivateRouteTable | |
# This creates the connection between the internet gateway and the public route table, without this the public route table | |
# won't have access to internet. | |
PublicRouteTableInternetRoute: | |
Type: AWS::EC2::Route | |
Properties: | |
DestinationCidrBlock: 0.0.0.0/0 | |
GatewayId: !Ref InternetGateway | |
RouteTableId: !Ref PublicRouteTable | |
# This is a security group that the NAT instance will use | |
NatSecurityGroup: | |
Type: AWS::EC2::SecurityGroup | |
Properties: | |
GroupDescription: Invoice NAT security group | |
VpcId: !Ref ServerlessVPC | |
# Here we allow incoming traffic only from within the VPC, meaning nothing outside the VPC can access it. | |
# I define all the ports in 0-1024 range, but you can specify only those that you are interested in. | |
# The block can be repeated multiple times if you want to include multiple port ranges. | |
SecurityGroupIngress: | |
- CidrIp: !GetAtt ServerlessVPC.CidrBlock | |
IpProtocol: tcp | |
FromPort: 0 | |
ToPort: 1024 | |
# Allow all outgoing traffic on every port on both tcp and udp. This is pretty permissive and in a more serious project | |
# it should probably be more restricted. | |
SecurityGroupEgress: | |
- CidrIp: 0.0.0.0/0 | |
IpProtocol: tcp | |
FromPort: 0 | |
ToPort: 65535 | |
- CidrIp: 0.0.0.0/0 | |
IpProtocol: udp | |
FromPort: 0 | |
ToPort: 65535 | |
Tags: | |
- Key: Name | |
Value: InvoiceApiNatSecurityGroup | |
# This is the EC2 instance that will handle all outgoing traffic from VPC. This is the only resource in this file that has | |
# runtime costs even when not using it. I chose the cheapest EC2 instance instead of NAT gateway because of the costs | |
# (around 5 USD for t3.nano instance vs around 40 USD). | |
# The image used (ami-0e7d350caf8ca5e8d) is provided by AWS exactly for this purpose, you can use it without changing anything. | |
NatEc2: | |
Type: AWS::EC2::Instance | |
Properties: | |
ImageId: ami-0e7d350caf8ca5e8d # The image provided by AWS | |
InstanceType: t3.nano # The cheapest instance | |
SourceDestCheck: false | |
# We need to associate it with the public subnet, the default one doesn't have internet access (isn't associated with internet gateway) | |
NetworkInterfaces: | |
- DeviceIndex: '0' | |
SubnetId: !Ref PublicSubnetA | |
# You need to set this to true because EC2 instances in non-default VPC don't have a public IP by default | |
AssociatePublicIpAddress: true | |
GroupSet: | |
- !Ref NatSecurityGroup # Reference the security group (firewall) created earlier | |
Tags: | |
- Key: Name | |
Value: InvoiceApiNatInstance | |
# Creating a subnet group for the aurora database I'm using, it needs to be associated with the private subnets | |
# for the Lambda to have access to it. Serverless Aurora doesn't have public internet access, only internal, | |
# and that's the whole reason this app was placed in VPC in the first place | |
AuroraSubnetGroup: | |
Type: AWS::RDS::DBSubnetGroup | |
Properties: | |
DBSubnetGroupDescription: Aurora Subnet Group | |
SubnetIds: | |
- Ref: PrivateSubnetA | |
- Ref: PrivateSubnetB | |
- Ref: PrivateSubnetC | |
Tags: | |
- Key: Name | |
Value: InvoiceApiDbSubnetGroup | |
# Some cluster configuration | |
AuroraRdsClusterParameter: | |
Type: AWS::RDS::DBClusterParameterGroup | |
Properties: | |
Description: Parameter group for the Serverless Aurora RDS DB. | |
Family: aurora5.6 | |
Parameters: | |
sql_mode: IGNORE_SPACE | |
max_connections: 100 | |
wait_timeout: 900 | |
interactive_timeout: 900 | |
Tags: | |
- Key: Name | |
Value: InvoiceApiDbClusterParameterGroup | |
# The Aurora database itself, it's ran in serverless mode which means it's scaled automatically based on the demand. | |
# Only Aurora can be used in serverless mode, all others need to be paid for full time when running even when not used. | |
AuroraRdsCluster: | |
Type: AWS::RDS::DBCluster | |
Properties: | |
Engine: aurora | |
EngineMode: serverless # set it to serverless mode | |
DatabaseName: invoice_api | |
MasterUsername: root | |
MasterUserPassword: ${env:REMOTE_MYSQL_PASSWORD} | |
ScalingConfiguration: | |
# This is important if you don't want to pay for the database when not using it, without this the database | |
# will have at leat the MinCapacity (in this case 1) units running. The downside is that it's pretty slow to start, | |
# can take even 40 seconds (and theoretically more). | |
AutoPause: true | |
MaxCapacity: 4 | |
MinCapacity: 1 | |
SecondsUntilAutoPause: 300 | |
DBSubnetGroupName: | |
Ref: AuroraSubnetGroup | |
DBClusterParameterGroupName: | |
Ref: AuroraRdsClusterParameter | |
# Create it in the default security group (access from everything within the VPC) | |
VpcSecurityGroupIds: | |
- !GetAtt ServerlessVPC.DefaultSecurityGroup | |
Tags: | |
- Key: Name | |
Value: InvoiceApiDatabase | |
# DynamoDB table used for caching, used in PAY_PER_REQUEST mode where you don't care about scaling and only pay | |
# for what you use. DynamoDB doesn't have internal endpoint meaning you always have to use internet to reach it. | |
# That's one of the reason I used NAT instance in the VPC (though for DynamoDB you can solve it using VPC endpoint and don't need NAT) | |
CacheTable: | |
Type: AWS::DynamoDB::Table | |
Properties: | |
AttributeDefinitions: | |
- AttributeName: id | |
AttributeType: S | |
BillingMode: PAY_PER_REQUEST # This sets it to be in mode where you only pay when you use it | |
TimeToLiveSpecification: | |
AttributeName: ttl | |
Enabled: true | |
KeySchema: | |
- AttributeName: id | |
KeyType: HASH | |
Tags: | |
- Key: Name | |
Value: InvoiceApiCacheTable | |
# This was used before and is not actually needed now but I didn't have yet time to clean up this configuration. | |
# When you configure a VPC endpoint for a resource, you don't need a NAT to access it. You don't even need to change anything | |
# in your app, the VPC handles it on its own. Sadly not all resources support VPC Endpoint, that's why I had to move to NAT instance. | |
CacheTableEndpoint: | |
Type: AWS::EC2::VPCEndpoint | |
Properties: | |
ServiceName: com.amazonaws.${self:provider.region}.dynamodb | |
VpcId: !Ref ServerlessVPC | |
RouteTableIds: | |
- !Ref PrivateRouteTable | |
PolicyDocument: { | |
"Version": "2008-10-17", | |
"Statement": [{ | |
"Action": "dynamodb:*", | |
"Effect": "Allow", | |
"Resource": !GetAtt CacheTable.Arn, | |
"Principal": "*" | |
}] | |
} | |
# The S3 bucket for storing files | |
UploadedInvoicesBucket: | |
Type: AWS::S3::Bucket | |
Properties: | |
BucketName: invoice-api-${self:provider.stage}-stored-files | |
# This is again a VPC endpoint, this time for the S3, same as the DynamoDB endpoint is not needed anymore | |
UploadedInvoicesBucketEndpoint: | |
Type: AWS::EC2::VPCEndpoint | |
Properties: | |
ServiceName: com.amazonaws.${self:provider.region}.s3 | |
VpcId: !Ref ServerlessVPC | |
RouteTableIds: | |
- !Ref PrivateRouteTable | |
PolicyDocument: { | |
"Version": "2012-10-17", | |
"Statement": [{ | |
"Action": "s3:*", | |
"Effect": "Allow", | |
"Resource": [ | |
!GetAtt UploadedInvoicesBucket.Arn, | |
!Join ['/', [!GetAtt UploadedInvoicesBucket.Arn, '*']] | |
], | |
"Principal": "*" | |
}] | |
} | |
# I have future plans to use SNS here which sadly doesn't support a VPC endpoint and thus I needed to create the NAT gateway. | |
# Basically your options for accessing both internal resources and internet: | |
# - separate parts of your app to different functions, the one that needs access to internal resources within VPN, the one that needs | |
# access to internet without one | |
# - use VPC Endpoints if the service you use supports them, they don't cost anything | |
# - use NAT, either instance or gateway, the downside is they cost you money even if your app is not used, small NAT instance is | |
# much cheaper than NAT gateway |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# This is a UI configuration part, the UI itself is written in Angular | |
service: invoice-ui | |
custom: | |
domain: '' # removed for this example | |
provider: | |
name: aws | |
stage: prod | |
region: eu-central-1 | |
runtime: provided | |
stackTags: | |
ClientName: 'My Name' | |
BillingProject: Invoice | |
BillingSubproject: Invoice-${self:provider.stage} | |
# Here I decided to not pack anything because I use this severless config pretty much only as a CloudFormation file | |
# and I find the deploy process of serverless much more pleasant than using plain aws cli, no function is created only the resources. | |
# The app is then compiled and deployed using a CI/CD tool with aws s3 sync command. | |
package: | |
exclude: | |
- ./** | |
resources: | |
Resources: | |
# The bucket where the html, css and js files are stored, synced during the deploy process | |
Website: | |
Type: AWS::S3::Bucket | |
Properties: | |
BucketName: invoice-ui-prod-website | |
CorsConfiguration: | |
CorsRules: | |
- AllowedHeaders: ["*"] | |
AllowedMethods: [GET] | |
AllowedOrigins: ["*"] | |
# In this policy I set it to allow public access from anywhere even without authentication | |
WebsiteBucketPolicy: | |
Type: AWS::S3::BucketPolicy | |
Properties: | |
Bucket: !Ref Website | |
PolicyDocument: | |
Statement: | |
- Effect: Allow | |
Principal: '*' # everyone | |
Action: 's3:GetObject' # to read | |
Resource: !Join ['/', [!GetAtt Website.Arn, '*']] | |
# I use the CDN as a webserver because https support with S3 public websites is not the best | |
WebsiteCDN: | |
Type: AWS::CloudFront::Distribution | |
Properties: | |
DistributionConfig: | |
Aliases: | |
- ${self:custom.domain} | |
Enabled: true | |
PriceClass: PriceClass_100 | |
HttpVersion: http2 | |
DefaultRootObject: index.html | |
Origins: | |
- Id: Website | |
DomainName: !GetAtt Website.RegionalDomainName | |
S3OriginConfig: {} # this key is required to tell CloudFront that this is an S3 origin, even though nothing is configured | |
DefaultCacheBehavior: | |
TargetOriginId: Website | |
AllowedMethods: [GET, HEAD] | |
ForwardedValues: | |
QueryString: 'false' | |
Cookies: | |
Forward: none | |
ViewerProtocolPolicy: redirect-to-https | |
Compress: true | |
CustomErrorResponses: | |
- ErrorCode: 500 | |
ErrorCachingMinTTL: 0 | |
- ErrorCode: 504 | |
ErrorCachingMinTTL: 0 | |
- ErrorCode: 404 # redirect not found error to index.html which allows me to deep link to the app | |
ResponsePagePath: /index.html | |
ErrorCachingMinTTL: 0 | |
ResponseCode: 200 | |
- ErrorCode: 403 # the same as above but for forbidden errors | |
ResponsePagePath: /index.html | |
ErrorCachingMinTTL: 0 | |
ResponseCode: 200 | |
# If you have custom domain in CloudFront, you need a custom SSL certificate | |
ViewerCertificate: | |
AcmCertificateArn: !Ref DomainCertificate | |
MinimumProtocolVersion: TLSv1.2_2019 | |
SslSupportMethod: sni-only | |
# This part is a little tricky because CloudFormation doesn't allow you to define resources in different regions in one | |
# deployment and CloudFront requires the certificate to be in us-east-1, while my app is in eu-central-1, | |
# thus I needed to use a custom function. This custom function is not created by me, I found it on GitHub, here | |
# are the installation steps: | |
# - git clone https://github.com/binxio/cfn-certificate-provider.git | |
# - cd cfn-certificate-provider | |
# - aws cloudformation deploy --capabilities CAPABILITY_IAM --stack-name cfn-certificate-provider --template-file cloudformation/cfn-resource-provider.yaml | |
# This will create the function in your account as Lambda and you can reference the function in a CloudFormation template, | |
# the above steps need to only be done once. | |
DomainCertificate: | |
Type: Custom::Certificate # The custom resource type | |
Properties: | |
DomainName: ${self:custom.domain} | |
ValidationMethod: DNS | |
Region: us-east-1 # The custom function allows us to specify a different region | |
# Every custom resource needs a service token which is the ARN of the function used | |
# The documentation of the custom function suggests using this: | |
# - !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:binxio-cfn-certificate-provider' | |
# But I couldn't get it to work in serverless (probably some parsing error due to how serverless variables are referenced) thus | |
# I replicated the same with the !Join function instead of !Sub | |
ServiceToken: !Join [':', ['arn:aws:lambda', !Ref AWS::Region, !Ref AWS::AccountId, 'function:binxio-cfn-certificate-provider']] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Just in case you're also interested in the deployment process, the most interesting part for you will probably be the publishing part | |
stages: | |
- dependencies | |
- test | |
- build | |
- publish | |
image: rikudousage/php-composer:7.4 | |
dependencies: | |
stage: dependencies | |
cache: | |
paths: | |
- vendor | |
- config/jwt | |
- bin/.phpunit | |
- pdf-server/node_modules | |
- node_modules | |
only: | |
changes: | |
- composer.lock | |
- composer.json | |
- .gitlab-ci.yml | |
- pdf-server/package.json | |
- pdf-server/yarn.lock | |
- yarn.lock | |
- package.json | |
except: | |
variables: | |
- $CI_COMMIT_MESSAGE =~ /.*skip (ci|dependencies|install).*/ | |
script: | |
- composer install | |
- cd pdf-server && yarn install && cd .. | |
- yarn install | |
- test -f config/jwt/private.prod.pem || openssl genpkey -out config/jwt/private.prod.pem -aes256 -algorithm rsa -pkeyopt rsa_keygen_bits:4096 -pass pass:$REMOTE_JWT_SECRET | |
- test -f config/jwt/public.prod.pem || openssl pkey -in config/jwt/private.prod.pem -out config/jwt/public.prod.pem -pubout -passin pass:$REMOTE_JWT_SECRET | |
- chmod 0644 config/jwt/*.pem | |
- test -d bin/.phpunit || composer phpunit || true | |
test: | |
stage: test | |
cache: | |
paths: | |
- vendor | |
- bin/.phpunit | |
policy: pull | |
only: | |
changes: | |
- .gitlab-ci.yml | |
- config/**/* | |
- public/**/* | |
- src/**/* | |
- .env | |
- .php_cs.dist | |
- composer.json | |
- composer.lock | |
- phpstan.neon | |
- tests/**/* | |
except: | |
variables: | |
- $CI_COMMIT_MESSAGE =~ /.*skip (ci|test|tests).*/ | |
before_script: | |
- echo "DATABASE_URL=mysql://root@localhost:3306/invoice_api" >> .env.local | |
- composer install --no-scripts | |
- php bin/console cache:warmup | |
- service mysql start | |
- php bin/console doctrine:database:create | |
- php bin/console doctrine:migrations:migrate | |
script: | |
- composer fixer -- --dry-run | |
- composer phpstan | |
- php bin/console doctrine:schema:validate | |
publish: | |
stage: publish | |
only: | |
refs: | |
- master | |
changes: | |
- .gitlab-ci.yml | |
- config/**/* | |
- php/**/* | |
- public/**/* | |
- src/**/* | |
- .env | |
- .php_cs.dist | |
- composer.json | |
- composer.lock | |
- phpstan.neon | |
- serverless.yml | |
- pdf-server/*.js | |
cache: | |
policy: pull | |
paths: | |
- vendor | |
- config/jwt | |
- pdf-server/node_modules | |
- node_modules | |
except: | |
variables: | |
- $CI_COMMIT_MESSAGE =~ /.*skip (ci|publish).*/ | |
before_script: | |
- composer install --no-dev --no-scripts -o -a | |
script: | |
# Some of these envs are needed to be defined before cache warmup so that the cache is valid even when deployed. | |
# The warming up of cache and deploying it allows the Symfony app to start really fast as it doesn't need to recreate | |
# the cache on every cold start making the first request really slow. | |
# If you use Twig in Symfony it might take more configuration than this, especially if you use render() twig function. | |
# Not all of these parameters are required for valid cache warmup, probably only APP_ENV and APP_SECRET | |
- echo "APP_ENV=prod" >> .env.local | |
- echo "APP_SECRET=$REMOTE_APP_SECRET" >> .env.local | |
- echo "DATABASE_URL=mysql://root:$REMOTE_MYSQL_PASSWORD@$REMOTE_MYSQL_HOST:3306/invoice_api" >> .env.local | |
- echo "JWT_PASSPHRASE=$REMOTE_JWT_SECRET" >> .env.local | |
- echo "JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.prod.pem" >> .env.local | |
- echo "JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.prod.pem" >> .env.local | |
- echo "DYNAMO_DB_TABLE=$REMOTE_DYNAMO_TABLE" >> .env.local | |
# here I create the cache for prod and ares environments | |
- php ./bin/console cache:warmup | |
- APP_ENV=ares APP_DEBUG=0 php bin/console cache:warmup | |
- composer dump-env prod | |
- serverless deploy | |
after_script: | |
- vendor/bin/bref cli invoice-api-prod-console --region eu-central-1 -- doctrine:migrations:migrate -n --all-or-nothing | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
stages: | |
- dependencies | |
- publish | |
image: rikudousage/php-composer:7.4 | |
dependencies: | |
cache: | |
paths: | |
- node_modules | |
only: | |
changes: | |
- yarn.lock | |
- .gitlab-ci.yml | |
except: | |
variables: | |
- $CI_COMMIT_MESSAGE =~ /.*skip (ci|dependencies|install).*/ | |
stage: dependencies | |
script: | |
- yarn install | |
# This publishes the javascript app, there's probably much better way to do this (since serverless originated in javascript world) | |
# but I'm not a javascript developer and this works for me so I didn't check. | |
publish_ui: | |
cache: | |
policy: pull | |
paths: | |
- node_modules | |
except: | |
variables: | |
- $CI_COMMIT_MESSAGE =~ /.*skip (ci|publish|ui).*/ | |
stage: publish | |
only: | |
changes: | |
- src/**/* | |
- .gitlab-ci.yml | |
script: | |
# Here I build the angular app | |
- ./node_modules/.bin/ng build --prod | |
# Here I sync it with the S3 bucket deleting all old files | |
- aws s3 sync dist/browser/ s3://invoice-ui-prod-website/ --delete | |
# Because the CloudFront caches the requests, everything that doesn't change its address needs to be invalidated | |
- aws cloudfront create-invalidation --distribution-id 'DISTRIBUTION_ID' --paths "/index.html" "/assets/languages/*" | |
# This just deploys the resources to CloudFormation in case something changed. | |
publish_serverless: | |
except: | |
variables: | |
- $CI_COMMIT_MESSAGE =~ /.*skip (ci|publish|serverless).*/ | |
stage: publish | |
only: | |
changes: | |
- .gitlab-ci.yml | |
- serverless.yml | |
script: | |
- serverless deploy |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment