Skip to content

Instantly share code, notes, and snippets.

@RikudouSage
Last active August 24, 2021 02:53
Show Gist options
  • Save RikudouSage/60ade9ec2e24ebd408951e8854973518 to your computer and use it in GitHub Desktop.
Save RikudouSage/60ade9ec2e24ebd408951e8854973518 to your computer and use it in GitHub Desktop.
serverless.yml complex example
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 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']]
# 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
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