[{"content":"This post was partially written by and edited using ChatGPT.\nLast month the Resolver development team had their first official hack week, which was dedicated to checking out the potential to improve our software with the help of OpenAI.\nWhat is OpenAI? You\u0026rsquo;ve probably heard of OpenAI since their ChatGPT program has made headlines frequently in recent months.\nAccording to my assistant editor:\nOpenAI is an artificial intelligence research laboratory consisting of a team of experts and researchers in the field of machine learning, natural language processing, robotics, and other related areas. The organization aims to develop advanced artificial intelligence in a safe and beneficial manner to benefit humanity as a whole. They conduct cutting-edge research and also develop various AI-based products and tools such as language models, chatbots, and robotics applications.\nChatGPT\nHow could OpenAI help us? Prior to getting stuck in and integrating with OpenAI\u0026rsquo;s APIs, we outlined our aims:\nImprove accessibility for consumers Lower overhead for engineering Simplify dependencies on external services These aims inspired several prototype projects.\nSimplification of our data infrastructure Within our platform, we employ an extensive suite of applications to process data. One of these applications is sentiment analysis, which detects emotions in text.\nWe found that ChatGPT can accurately extract sentiment from text with minimal technological barriers. As a result, we can potentially replace a significant amount of infrastructure with simple API calls.\nWriting formal emails based on question prompts We discovered that ChatGPT could compose formal emails to companies using user input obtained from simple forms. However, a drawback was that it often editorialised complaints by adding information not present in the provided data.\nWe believe that supplying additional contextual messages could constrain the language model to adhere to the given information. However, we also anticipate that the output would still require editing and approval by the end-user.\nAccessibility improvement for case management Raising and managing cases over the phone Currently, our applications can only be accessed through a web browser, which can be restrictive for some users. Although automated phone lines have been around for a while (since the late \u0026rsquo;70s), their rigid structure may not always accommodate the complexity of case information.\nWe discovered that by using the OpenAI \u0026ldquo;edits\u0026rdquo; API, we could analyse transcriptions of phone calls and map them to a JSON structure, enabling us to handle cases discussed over the phone or on video calls more efficiently. The generated JSON could then be submitted to our API, along with the original transcription to help with reviewing its accuracy.\nSummarising cases Complaint and appeal cases often keep growing until they are resolved. This results in long, hard to follow message threads with lots of important information in them.\nChatGPT\u0026rsquo;s natural language processing capabilities make it particularly adept at summarising large amounts of information into a succinct format. By leveraging this strength, we were able to efficiently transform complex case details into a more digestible format, allowing our customer service team to quickly and accurately assess the current state of a case.\nWhat did we discover? ChatGPT is a language model developed by OpenAI that uses deep learning techniques to generate human-like responses to text-based prompts. GPT stands for \u0026ldquo;Generative Pre-trained Transformer,\u0026rdquo; which refers to the model architecture and the pre-training process it undergoes to learn the patterns and structure of natural language.\nChatGPT\nSubjectivity and objectivity Upon initial experimentation, we observed that ChatGPT would often respond with the statement \u0026ldquo;As an AI language model, I do not hold personal beliefs or opinions\u0026rdquo; when posed with questions that may have had subjective answers. This limitation serves as a reminder that ChatGPT is a language model that lacks the ability to think autonomously. As a result, we shifted our focus towards using ChatGPT in a more objective manner, which led us away from considering it as a potential mediator for automated complaints.\nChatGPT excels at extracting and processing data from free-form human-written text. It accurately converts the extracted information into formats that can be processed by our internal applications. This was particularly useful in automating complaint creation from recorded telephone calls, where ChatGPT consistently extracted key issues and requested redresses from transcripts, and structured them to fit our platform\u0026rsquo;s API.\nStructuring your messages The \u0026ldquo;edits\u0026rdquo; API by OpenAI includes a crucial feature that enables users to provide context for their messages. This additional information helps the language model process the request more accurately.\nThis can be achieved by sending \u0026ldquo;system\u0026rdquo; and \u0026ldquo;user\u0026rdquo; messages to the API:\nopenai.ChatCompletionRequest{ Model: openai.GPT3Dot5Turbo, Messages: []openai.ChatCompletionMessage{ // System messages: { Role: openai.ChatMessageRoleSystem, Content: \u0026#34;The initiating entity is the CALLER.\u0026#34;, }, { Role: openai.ChatMessageRoleSystem, Content: \u0026#34;Financial redresses must be a number with two decimal places.\u0026#34;, }, { Role: openai.ChatMessageRoleSystem, Content: \u0026#34;Please use the following message to fill out this JSON struct: \u0026#34; + jsonTemplate, }, // User messages: { Role: openai.ChatMessageRoleUser, Content: \u0026#34;Insert transcription here\u0026#34;, }, }, } It appears that the \u0026ldquo;system\u0026rdquo; role influences how the language model will respond, and \u0026ldquo;user\u0026rdquo; messages are prompts for it to process.\nData security concerns As a company, we place great emphasis on ensuring the security of our users\u0026rsquo; data. However, using an external platform such as OpenAI poses a challenge because we can\u0026rsquo;t control how our data is handled, which conflicts with our company ethos.\nWhile OpenAI\u0026rsquo;s platform is beneficial for processing text, any implementation of it in our own platform would require user opt-in. If refused, we would have to revert to our existing applications, resulting in no simplification of our infrastructure.\nThe API\u0026rsquo;s output is only as good as your input ChatGPT\u0026rsquo;s responses rely heavily on the prompts it is given. As a language model, it does not inherently possess knowledge but is trained to learn and predict the patterns in the language based on the vast amount of data it has been trained on. Therefore, the quality of its output heavily depends on the quality of the input prompts provided to it. It can only generate responses based on the information and context it is given. The more precise and informative the prompts are, the better the responses will be. Therefore, it is crucial to frame the prompts in such a way that the language model can accurately understand and generate output that meets the desired objectives.\nConclusion In our opinion, ChatGPT and OpenAI have the potential to enhance some of Resolver\u0026rsquo;s processes. However, it would necessitate end-users to opt-in before using them. Although these tools could generate output with considerable accuracy, it is still vital to note that it cannot replace the importance of human interaction.\nMoreover, we anticipate that the output of ChatGPT and OpenAI would require extensive monitoring and review to ensure that the generated output meets the company\u0026rsquo;s standards. Therefore, while we recognise the potential benefits of these tools, we also acknowledge that it cannot replace the importance of human judgement in many aspects.\n","permalink":"https://engineering.resolvergroup.com/2023/03/hack-week-2023/","summary":"\u003cp\u003e\u003cem\u003eThis post was partially written by and edited using \u003ca href=\"https://openai.com/blog/chatgpt\"\u003eChatGPT\u003c/a\u003e.\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eLast month the Resolver development team had their first official hack week, which was dedicated to checking out the potential to improve our software with the help of \u003ca href=\"https://openai.com/\"\u003eOpenAI\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"what-is-openai\"\u003eWhat is OpenAI?\u003c/h2\u003e\n\u003cp\u003eYou\u0026rsquo;ve probably heard of OpenAI since their ChatGPT program has made headlines frequently in recent months.\u003c/p\u003e\n\u003cp\u003eAccording to my assistant editor:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eOpenAI is an artificial intelligence research laboratory consisting of a team of experts and researchers in the field of machine learning, natural language processing, robotics, and other related areas. The organization aims to develop advanced artificial intelligence in a safe and beneficial manner to benefit humanity as a whole. They conduct cutting-edge research and also develop various AI-based products and tools such as language models, chatbots, and robotics applications.\u003c/p\u003e\n\u003cp\u003e\u003cem\u003eChatGPT\u003c/em\u003e\u003c/p\u003e\n\u003c/blockquote\u003e","title":"Hack week 2023: OpenAI for dispute resolution"},{"content":"What is an API? API stands for Application Programming Interface, let\u0026rsquo;s break that down:\nInterface - Every device we use has some kind of interface. Your TV has a remote with buttons on it, while a light switch has an even more straightforward interface- on or off. We use these interfaces to get the device to do what we want.\nApplication Programming - The Application Programming bit is easier to understand. An API is how applications talk to each other. All software that you can interact with through code has some form of an API.\nAn application programming interface is a set of rules that define how computers, applications, or machines can talk to each other. You can think of it this way: the typical user interface is intended for use by a human being, while APIs are intended for use by an application or computer.\nHow do web APIs work? You may not know this but your web browser has plenty of APIs built into it that we can use! These are called Web APIs.\nSome APIs let your app view weather forecasts, show the latest sports scores, respond to video game controllers, and lots more!\nThese are APIs that sit between your code and make requests to data sources or functionality on a server that you\u0026rsquo;d like to access.\nThe API generally does two things:\nIt sets rules for interacting with it The \u0026ldquo;rules\u0026rdquo; are the API saying \u0026ldquo;Structure your request like this, and I\u0026rsquo;ll send you data structured like this.\u0026rdquo; Badly structured requests result in errors.\nIt handles data transfer between the server and the code making the request. The API is a program that acts as a middleman between the web app and the server and database Once it receives a valid request, it will run. Depending on what you ask for, it might return an image, some data, or just let you know that it successfully received your request.\nOk so just to recap. APIs consists of 3 basic steps:\nThe initial request. Normally via a URL The API handles the rules for interacting with the server/database The results that return. Normally as JSON Working with JSON Most HTTP APIs will take and receive data in the JSON (JavaScript Object Notation) format. It makes learning how to read and write using this format a pretty essential skill when working with APIs. JSON keeps its data in key-value pairs\nThis is an example of a JSON output:\n{ \u0026#34;name\u0026#34;: \u0026#34;John\u0026#34;, \u0026#34;age\u0026#34;: 30, \u0026#34;car\u0026#34;: null } It defines an object with 3 properties, name, age, and car.\nSome API concepts Endpoints - Simply put, an endpoint is one end of a communication channel. When an API interacts with another system, the touchpoints of this communication are considered endpoints. For APIs, an endpoint can include a URL of a server or service. Each endpoint is the location from which APIs can access the resources they need to carry out their function.\nAPIs work using ‘requests’ and ‘responses.’ When an API requests information from a web application or web server, it will receive a response. The place that APIs send requests and where the resource lives are called endpoints.\nAuthentication - Some APIs require you to sign up for an account or obtain a unique key to access their information. It might be to block anonymous traffic, prevent abuse of the service, or because they want to charge a fee for the data.\nWith authentication, you pass the API a secret key that identifies a specific user or application request. The server can then determine if you\u0026rsquo;re able to access the data or not.\nHTTP Verbs - With each HTTP request created, there is always an \u0026lsquo;HTTP Verb\u0026rsquo; that goes along with it. The most common are POST, GET, PUT, and DELETE. These correspond to create, read, update, and delete (or CRUD) operations, respectively.\nTroubleshooting Eventually, you\u0026rsquo;ll run into some trouble with APIs, and you\u0026rsquo;ll need to debug a problem in your code. If that happens, here are some helpful tips.\nCheck the documentation - There should be documentation to tell you how to structure your request. Make sure you\u0026rsquo;re following the syntax described there. Compare their examples to what you have in your request to see what\u0026rsquo;s different.\nCheck the network tab - If you\u0026rsquo;re making your API requests in the browser, one of the best API troubleshooting tools is the Network tab. open DevTools. Click the Network tab at the top. Now the Network tab will listen to every single request that the website makes.\nCheck the status code - You\u0026rsquo;ve seen \u0026ldquo;404 File Not Found\u0026rdquo; on a website when you clicked a dead link or typed something incorrectly. That 404 is a specific code that the server gives to your browser as feedback on its request. It\u0026rsquo;s one of many HTTP status codes that help us understand how our requests are being received. The responses are grouped into hundreds:\n1xx informational response - the request was received, continuing process 2xx successful - the request was successfully received, understood and accepted 3xx redirect - further action needs to be taken in order to complete the request 4xx client error - the request contains bad syntax or cannot be fulfilled Conclusion While some APIs can be incredibly complex, the underlying notions remain the same, hopefully, this helps to take some of the mystery away. We touched on some key concepts and explained what makes APIs such powerful tools in web development.\nAs always practice is important and there are many free resources and tutorials out there to help you continue your API adventures.\nGood luck talking with robots!\n","permalink":"https://engineering.resolvergroup.com/2022/03/talking-robots-lets-talk-about-apis/","summary":"\u003ch2 id=\"what-is-an-api\"\u003eWhat is an API?\u003c/h2\u003e\n\u003cp\u003eAPI stands for Application Programming Interface, let\u0026rsquo;s break that down:\u003c/p\u003e\n\u003cp\u003eInterface - Every device we use has some kind of interface. Your TV has a remote with buttons on it, while a light switch has an even more straightforward interface- on or off. We use these interfaces to get the device to do what we want.\u003c/p\u003e\n\u003cp\u003eApplication Programming - The Application Programming bit is easier to understand. An API is how applications talk to each other. All software that you can interact with through code has some form of an API.\u003c/p\u003e","title":"Talking robots! Let's talk about APIs"},{"content":"Have you ever tried to make the download filename of an AWS S3 object be in Korean? No…? Being a developer who, until recently, had only written in English, I hadn\u0026rsquo;t either. However, it turns out using some UTF-8 characters in S3 object request parameters can lead to some interesting errors.\nHow to Change the Filename of a Download When downloading a file directly from the browser using a HTML anchor tag, the download=\u0026lt;filename\u0026gt; attribute can be used. However, the attribute will only work for same-origin URLs and the blob: and data: schemes. For example:\n\u0026lt;a href=\u0026#34;https://example.com/image.png\u0026#34; download=\u0026#34;painting.png\u0026#34;\u0026gt;Download painting\u0026lt;a/\u0026gt; For situations where the application CDN (CloudFront) is not set up with the same origin or when download request are not made directly by HTML link, such as when clicking a link in an email, the Content-Disposition header can be used to set the download filename. The header is returned in the HTTP response and indicates to the client the filename and whether the content is expected to be displayed inline or downloaded as an attachment.\nThe Content-Disposition header takes the form:\nContent-Disposition: attachment; filename=painting.png; Setting Content-Disposition in AWS S3 HTTP Response S3 provides multiple ways to set the Content-Disposition header of an object being downloaded, two of the main ways are:\nSet Content Disposition parameter on upload - works for new objects Set response-content-disposition parameter in request - works for an existing object however requires a signed URL In this blog post we will look at setting the response-content-disposition parameter in request, which has the following syntax (excluding signed request parameters):\nGET /\u0026lt;object-key\u0026gt;?response-content-disposition=\u0026lt;content-disposition\u0026gt;\u0026amp;response-content-type=\u0026lt;content-type\u0026gt; Sample Repository\nYou can test out a request with the above syntax by running the first example in the sample repository I’ve made, provided here. The sample repository includes a basic AWS S3 infrastructure with CloudFront CDN for signed URLs, along with some scripts for generating the URLs outlined in this post. The readme has details on how to set it up and run the examples.\nThe object uploaded to S3 during the infrastructure creation is an image of my cat, Jessie, however the object will be named \u0026rsquo;not-a-cat\u0026rsquo;, so you will know if the Content-Disposition HTTP header has worked correctly.\nThe first example request uses:\ncontent disposition: attachment; filename=jessie.jpg; content type: image/jpg The Problem with UTF-8 in the \u0026lsquo;response-content-disposition\u0026rsquo; Parameter Let\u0026rsquo;s try setting the response-content-disposition parameter to the Korean translation of cat, which is \u0026ldquo;고양이.jpg\u0026rdquo;.\nresponse-content-disposition=attachment; filename=고양이.jpg; However, setting the response-content-disposition parameter filename in the S3 request as above won\u0026rsquo;t work.\nFirstly, as per RFC2396 2.4 all data must be escaped to a printable unreserved character of the US-ASCII coded character set. Therefore, the URL must be URL encoded, converting any non-valid characters into US-ASCII.\nAdditionally, UTF-8 encoded characters, such as those in the Korean alphabet, are not typically allowed in HTTP header fields. This is due to the HTTP specification only allowing field content with the text in ISO-8859-1, see RFC-7230 3.2.4. While most modern browsers and application allow the use of non-ISO-8859-1 characters in HTTP fields, some do not. For example, support for UTF-8 headers was only recently added to the Puma application server, see the changelog here.\nFortunately, as described in RFC6266, the Content-Disposition field allows for an additional filename* parameter which uses the encoding defined in RFC8187, allowing for the use of characters not present in the ISO-8859-1 character set.\nSo, to allow for backwards combability with legacy applications, a pure ISO-8859-1 filename parameter must be supplied, along with UTF-8 encoded filename* parameter. Leading to Content-Disposition field being described as:\nContent-Disposition: attachment; filename=jessie.jpg; filename*= UTF-8\u0026#39;\u0026#39;고양이.jpg; When the S3 object request is URL encoded, see example 2 in the sample repository, the request will look like:\nGET https://example.cloudfront.net/not-at-cat.jpg?response-content-disposition=attachment%3B+filename%3D%22jessie.jpg%22%3B+filename%2A%3D+UTF-8%27%27%22%EA%B3%A0%EC%96%91%EC%9D%B4.jpg%22 However, performing this request results in an error from S3 😭\nThe problem here is that S3 decodes the URL encoded URL, then attempts to return the value of the response-content-disposition parameter in the HTTP response Content-Disposition header field. But, since the filename is no longer URL encoded and contained within ISO-8859-1 character set, AWS S3 throws an error.\nHow to Set a UTF-8 in the \u0026lsquo;response-content-disposition\u0026rsquo; Parameter The solution to this issue is rather simple, URL encode the UTF-8 filename twice in the request. However, this took me a fair bit of debugging to arrive at, hence this blog post to save you time if you come across the same issue.\nThe reason for encoding the UTF-8 filename twice, is to ensure the Content-Disposition handled by S3 will remain in the US-ASCII character set and therefore the ISO-8859-1 character set. It will then be decoded into UTF-8 by the browser on receipt.\nFollowing on from the examples above, example 3 encodes only the UTF-8 filename* parameter twice using the following Ruby code:\nrequire \u0026#39;cgi\u0026#39; filename_legacy = \u0026#34;jessie.jpg\u0026#34; filename_utf8 = \u0026#34;고양이.jpg\u0026#34; filename_utf8_url_encoded = CGI.escape(filename_utf8) content_disposition = CGI.escape(\u0026#34;attachment; filename=#{filename_legacy}; filename*= UTF-8\u0026#39;\u0026#39;#{filename_utf8_url_encoded};\u0026#34;) Which results in the URL:\nGET https://example.cloudfront.net/not-at-cat.jpg?response-content-disposition=attachment%3B+filename%3Djessie.jpg%3B+filename%2A%3D+UTF-8%27%27%25EA%25B3%25A0%25EC%2596%2591%25EC%259D%25B4.jpg%3B\u0026amp;response-content-type=image/jpeg Performing the above request will result in a file with the desired filename being downloaded, whether the browser is new or legacy.\nReproduced with permission from Chris Wood\u0026rsquo;s personal blog\n","permalink":"https://engineering.resolvergroup.com/2022/02/aws-s3-utf-8-content-disposition/","summary":"\u003cp\u003eHave you ever tried to make the download filename of an AWS S3 object be in Korean? No…? Being a developer who, until recently, had only written in English, I hadn\u0026rsquo;t either. However, it turns out using some UTF-8 characters in S3 object request parameters can lead to some interesting errors.\u003c/p\u003e\n\u003ch2 id=\"how-to-change-the-filename-of-a-download\"\u003eHow to Change the Filename of a Download\u003c/h2\u003e\n\u003cp\u003eWhen downloading a file directly from the browser using a HTML anchor tag, the \u003ccode\u003edownload=\u0026lt;filename\u0026gt;\u003c/code\u003e attribute can be used. However, the attribute will only work for same-origin URLs and the \u003ccode\u003eblob:\u003c/code\u003e and \u003ccode\u003edata:\u003c/code\u003e schemes. For example:\u003c/p\u003e","title":"AWS S3 UTF-8 Content Disposition"},{"content":"What is ECS? AWS Elastic Container Service (ECS) is a fast and popular way to orchestrate containerised applications in AWS\u0026rsquo;s cloud computing platform. ECS comes with autoscaling baked in and natively runs containers on AWS\u0026rsquo;s Fargate serverless compute engine. What makes ECS particularly nice to use is that it abstracts away much of the operations management that comes along with rolling your own deployment, either on-site or on cloud-based computing, such as AWS EC2.\nThis article assumes you know the basics of ECS terminology and architecture. If you don\u0026rsquo;t, it\u0026rsquo;s one of the many fantastic ways to deploy applications on AWS, and I recommend you check out the AWS ECS docs.\nWhere is the Deployment Rollback Command? While refactoring the deployment scripts for a Resolver project managed using ECS, I noticed something was missing in the AWS ECS CLI and UI console: a rollback deployment command… 😱\nBut why is it missing when other services, e.g. Heroku, have these commands? It turns out the reason there is no standard rollback command in the ECS service is due to how ECS handles deployments. New deployments occur when:\na new service is created with a task definition a service\u0026rsquo;s task definition is updated a service re-deployment is forced, such as when it is updated using the aws ecs update-service command with the --force-new-deployment option passed When a new deployment is triggered, the ECS scheduler starts new containers using the service\u0026rsquo;s task definition and stops old containers running previous versions, moving traffic to the new containers as they become healthy.\nAs newly created containers are created from the container image defined in the task definition, deployments are very flexible. Since the image tag can be set to an arbitrary value, it might not change between deployments; however, the actual image pulled by ECS can, making a rollback command not trivial to implement.\nFor example, in the project I was refactoring, the image being pulled was tagged as the latest image from a related Elastic Container Registry (ECR) repository. While pulling the latest image made infrastructure setup and deployments relatively straightforward (new images could be pushed to ECR and the service force re-deployed), it made rollbacks rather tricky. Some of the ways rollbacks were previously performed were:\ndeleting the latest image, retagging an old image as latest, and force re-deploying reverting the git commits on GitHub and deploying the new image Neither of the above was quick, provided a good indication of which version of the app was deployed, or allowed the original deployment to be returned to again.\nDeploying To rollback effectively, first, you must deploy the application correctly and build and tag new container images suitably.\nTo start, when building container images, they should be tagged and pushed to the repository with a unique identifier. For example, typically, the SHA-1 hash of the latest git commit of the codebase is used as it provides a clear indication of the state of the codebase described in an image. The git hash can also be used to fetch commit messages for a more human-readable representation of where rollbacks will revert to.\nFor a sample hello world ECS application (full codebase can be seen here), an application image can be built, tagged with a unique ID, and pushed to a remote repository:\n# ./bin/deploy # ... ecr_repo=\u0026lt;your ecr repository url\u0026gt; unique_image_tag=$(git rev-parse --short HEAD) docker build \\ --tag \u0026#34;$ecr_repo:$unique_image_tag\u0026#34; \\ --tag \u0026#34;$ecr_repo:latest\u0026#34; \\ . aws ecr get-login-password --profile ecs-rollback-hello-world | \\ docker login --username AWS --password-stdin \u0026#34;$ecr_repo\u0026#34; docker push \u0026#34;$ecr_repo:$unique_image_tag\u0026#34; docker push \u0026#34;$ecr_repo:latest\u0026#34; Once the updated images are pushed, the task definition can be updated with the new tag. Since task definitions are versioned, the services are then updated with the new revision number, triggering a deployment of the new tasks:\n# ./bin/deploy # ... # fetch current task definition current_task_definition=$( aws ecs describe-task-definition \\ --task-definition \u0026#34;$task_definition_family\u0026#34; \\ --query \u0026#39;{ containerDefinitions: taskDefinition.containerDefinitions, family: taskDefinition.family, executionRoleArn: taskDefinition.executionRoleArn, networkMode: taskDefinition.networkMode, volumes: taskDefinition.volumes, placementConstraints: taskDefinition.placementConstraints, requiresCompatibilities: taskDefinition.requiresCompatibilities, cpu: taskDefinition.cpu, memory: taskDefinition.memory }\u0026#39; ) current_task_definition_revision=$( aws ecs describe-task-definition --task-definition \u0026#34;$task_definition_family\u0026#34; \\ --query \u0026#39;taskDefinition.revision\u0026#39; ) # compare current and updated image tags current_container_image=\u0026#34;$(echo \u0026#34;$current_task_definition\u0026#34; | jq .containerDefinitions[0].image)\u0026#34; updated_container_image=\u0026#34;$ecr_repo:$unique_image_tag\u0026#34; if [[ $current_container_image = \u0026#34;\\\u0026#34;$updated_container_image\\\u0026#34;\u0026#34; ]]; then echo \u0026#34;Container image \u0026#39;$unique_image_tag\u0026#39; already defined in the latest task definition revision: $task_definition_family:$current_task_definition_revision\u0026#34; read -p \u0026#34;Are you sure you want to deploy?\u0026#34; -n 1 -r if [[ ! $REPLY =~ ^[Yy]$ ]] then exit 1 fi fi # inject new image tag into task definition and update updated_task_definition=$( echo \u0026#34;$current_task_definition\u0026#34; | jq --arg CONTAINER_IMAGE \u0026#34;$updated_container_image\u0026#34; \u0026#39;.containerDefinitions[0].image = $CONTAINER_IMAGE\u0026#39; ) updated_task_definition_info=$(aws ecs register-task-definition --cli-input-json \u0026#34;$updated_task_definition\u0026#34;) # update service with new task definition revision updated_task_definition_revision=$(echo \u0026#34;$updated_task_definition_info\u0026#34; | jq \u0026#39;.taskDefinition.revision\u0026#39;) aws ecs update-service --cluster \u0026#34;$ecs_cluster_name\u0026#34; \\ --service \u0026#34;$ecs_service_name\u0026#34; \\ --task-definition \u0026#34;$task_definition_family:$updated_task_definition_revision\u0026#34; \\ \u0026gt;/dev/null Rolling Back Now new deployments are updating task definitions with unique image tags, rollbacks are a case of updating a service with a previous revision number and rolling back any data migrations or schema changes. Changing a service\u0026rsquo;s task definition revision number can be done in various ways:\nAWS ECS UI Console The UI console has an interface to update services:\nFirst, visit the service page and click Edit service:\nChange task definition revision number to a previous version.\nClick Update to update the service and re-deploy.\nPerformed manually using the CLI With project specific variables:\n# list task definitions aws ecs list-task-definitions \\ --family-prefix \u0026#34;$task_definition_family\u0026#34; \\ --query taskDefinitionArns \\ --sort DESC # update service with previous task definition arn aws ecs update-service \\ --cluster \u0026#34;$ecs_cluster_name\u0026#34; \\ --service \u0026#34;$ecs_service_name\u0026#34; \\ --task-definition \u0026#34;$task_definition_arn\u0026#34; Scripted Rolling back can be scripted to reduce human error. For example, take a look at the script I made for manually rolling back deploys, which lets the user:\nSelect a cluster in the current AWS profile\u0026rsquo;s region. Select some services from the selected cluster. Select a task definition to rollback to from a list of the last n task definitions for each selected service, along with their git commit message information Updates service with new task definition and re-deploys, rolling back the application state aws_ecs_rollback\nRun it in the sample ECS application using ./bin/rollback_dialog\nConclusion While there are various ways to rollback ECS deployments when using integrated tooling such as AWS\u0026rsquo;s CodeDeploy, if you are working on a project without a nicely configured CI/CD pipeline, having a process to manually rollback is crucial during the interim while the project is refactored.\nHopefully, this article helps you set one up for your project, so you can deploy new application revisions without the worry of a slow rollback in the rare case something goes wrong.\nThis blog is taken from Chris\u0026rsquo;s personal blog on October 11th 2021. The original post can be seen here.\n","permalink":"https://engineering.resolvergroup.com/2021/10/rolling-back-aws-elastic-container-service-ecs-deployments/","summary":"\u003ch2 id=\"what-is-ecs\"\u003eWhat is ECS?\u003c/h2\u003e\n\u003cp\u003eAWS Elastic Container Service (ECS) is a fast and popular way to orchestrate containerised applications in AWS\u0026rsquo;s cloud computing platform. ECS comes with autoscaling baked in and natively runs containers on AWS\u0026rsquo;s Fargate serverless compute engine. What makes ECS particularly nice to use is that it abstracts away much of the operations management that comes along with rolling your own deployment, either on-site or on cloud-based computing, such as AWS EC2.\u003c/p\u003e","title":"Rolling back AWS Elastic Container Service (ECS) Deployments"},{"content":"Introduction Whenever a Ruby on Rails developer hears the phrase “New Rails update!” they will no doubt experience a mixture of excitement and fear. It’s always very exciting to see the new tools that come with a new Rails version and all the new possibilities that go with it. But at the same time, you know that you are bound to have some hiccups with some legacy code or some old gem that you use on your application. Nevertheless, I decided it was time to upgrade our Rails app.\nNeedless to say, we will be following the guides from guides.rubyonrails.org on how to upgrade from Rails 5.2 to 6.0.\nGetting started Before updating anything on your app, make sure that you have a good amount of tests! In our case, our test coverage is decent and we also have a good system for tracking bugs, so even if all of our tests pass we still know if a new bug shows up.\nJust like any other gem update, I opened my Gemfile, changed the Rails version 'Rails', '~\u0026gt; 6.0.0' and ran bundle update Rails. As expected, I got loads of \u0026quot;Bundler could not find compatible versions for gem\u0026quot; errors, but luckily I only had to update two gems manually and many other gems were updated automatically. And done! Now we were ready to upgrade to Rails 6.0… or were we?\nTesting You can’t say you\u0026rsquo;ve successfully upgraded Rails if your application doesn’t work anymore. So we have to ensure the functionality is the same as it was before, and what better option do we have to check than testing?\nAfter running our automated tests we had multiple tests failing and lots of deprecation warnings. Most of the deprecations and errors were very straightforward to fix - for example, update_attributes had to be changed to update, and the case_sensitive default value for presence validations was going to change, so it had to be specified, etc. But, after fixing all the failing tests, we finally had our Rails app upgraded! \u0026hellip;well, not quite.\nRevisiting the Rails guides It’s always a good idea to revisit the guides and check you haven\u0026rsquo;t missed anything. In my case I had missed two updates.\nThe first one was the force_ssl option, which enforces https. Previously, it could be configured in the controllers, but this has been removed in Rails 6.1. In our application we were turning it off on a couple of actions, and after some investigation I realised that we should keep it on at all times and move the configuration into the production and staging environment configuration files.\nThe other one was the cookie encryption change. In Rails 6.0, the purpose and expiry metadata are embedded into the cookie to make it more secure, but this change is not compatible with older versions of Rails. Since we were not sure if we would have to rollback the change, I followed the guide recommendation and set Rails.application.config.action_dispatch.use_authenticated_cookie_encryption = false until we were sure that we were sticking with Rails 6.0.\nAnother new addition caught my attention - the Zeitweirk autoloading. I tried adding it into our application but failed miserably. That is because we are using a very old gem that does not follow the right file naming convention, so for now we are still using the classic Rails autoloading.\nConclusion Even though we are not yet using all that Rails 6.0 has to offer, I am satisfied with the upgrade and think we have made the right decision.\nFinally, it was time to create the PR, check all the tests were passing, and get the approval of the other developers.\nThe PR was merged, and our app is now finally upgraded to Rails 6.0!\n","permalink":"https://engineering.resolvergroup.com/2021/09/upgrading-to-rails-6-0/","summary":"\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eWhenever a Ruby on Rails developer hears the phrase “New Rails update!” they will no doubt experience a mixture of excitement and fear. It’s always very exciting to see the new tools that come with a new Rails version and all the new possibilities that go with it. But at the same time, you know that you are bound to have some hiccups with some legacy code or some old gem that you use on your application. Nevertheless, I decided it was time to upgrade our Rails app.\u003c/p\u003e","title":"Upgrading to Rails 6.0"},{"content":"Back in July of this year, our Product Manager here in the Consumer Team at Resolver moved on to a new job. This was a great loss both for us and the wider company and we knew that it would take several months to find and recruit the right person for the job. During the interim, it was decided that we would try to become somewhat self-sufficient, with our Tech Lead, CTO, and Head of Content all pitching in and directing our efforts over the following few months.\nUp until this point, we had been managing our workload by having weekly meetings, led by our Product Manager, where we would discuss and prioritize incoming work. The downside to this approach was that, no matter how hard we worked, the to-do list always seemed to be growing. Sometimes, the list would get so long in fact, that we would need to refine it and move several tickets into a backlog. For visual learners like myself, this could be somewhat demotivating as the dopamine hit from finishing a ticket quickly disappeared when browsing Trello to find a seemingly never-ending list of items to work on.\nWe decided that this could be the perfect time to experiment and to try a new way of working. We looked into Shapeup, the brainchild of Basecamp, which promises to help teams ‘stop running in circles and ship work that matters’. Using Shapeup, Pitches are proposed as projects to work on during strict six-week stretches. Some pitches may be smaller and take less time, but never longer. If a pitch ends up over-running, it gets cancelled rather than extended. Whilst this sounds daunting, pitches are focused pieces of planned and thought-out work, with scopes small enough to ensure a thorough job is completed during each six-week cycle.\nFirst, we embarked on a transitional period where we worked through the remaining tickets on our to-do column. This was a very satisfying few weeks, ticking off various bugs and tasks we have been meaning to do for a long time until - drum roll please - one beautiful day our board looked like this:\nDuring those weeks, we didn’t feel it necessary to have catch-up meetings or planning sessions, seeing as we were not bringing in any new work. We also had a daily written stand-ups through Slack, so we knew what each other were working on. Whilst this enabled us to be super productive and remain focused, being a remote team, it also made me feel disconnected from the rest of the team. To combat this, we introduced a weekly coffee catch-up chat that brought us back together and was fundamental in keeping us socially connected.\nThroughout the six weeks, all the developers had a think about what we would like to work on, whether it be a bug fix, a new feature, or an improvement to an existing one. Once we formulated these ideas, we wrote these into a pitch and shared them with the wider team.\nThis then became a space where we could discuss the ideas with the wider company, often encouraging further exploration and bringing new ideas to the pitches. It also creates a space where ideas get explored at the formulation stage, rather than building something and having to re-shape it after deployment.\nWe held a betting table meeting with all stakeholders, where we discussed the pitches in relation to the current business needs and objectives and picked out several tasks to commit to working on and releasing within the upcoming six-week cycle. This ensures that everything we are working on is relevant to the business and our users and also that everyone in the team has the same context and understanding of why it is needed.\nThe pitches that did not get picked do not get moved to a backlog or stay on the pitches page, they are removed. This creates a clean slate and allows for new ideas to keep rolling in without judgment being clouded from less relevant ideas. Of course, if a pitch is still wanted in six weeks’ time, it can always be resurrected and pitched again in the next betting meeting. This also removes the mental burden of feeling like a long never-ending list of tasks is lurking and instead rewards movement and productivity.\nWe are just about to start our first six-week cycle and are all very excited and optimistic to see how it works out. I will check in and keep you updated with our progress. I feel we have done a great job at managing ourselves so far, however researching pitches and finding out feasibility takes a substantial amount of time. Whilst I have enjoyed being part of this process and getting an understanding of all that it involves, I have gained a vast appreciation of the role of a Product Manager and just how much is on their plate. I think we are all looking forward to hiring a replacement in the near future!\n","permalink":"https://engineering.resolvergroup.com/2021/09/a-developer-teams-approach-to-not-having-a-product-manager/","summary":"\u003cp\u003eBack in July of this year, our Product Manager here in the Consumer Team at Resolver moved on to a new job. This was a great loss both for us and the wider company and we knew that it would take several months to find and recruit the right person for the job. During the interim, it was decided that we would try to become somewhat self-sufficient, with our Tech Lead, CTO, and Head of Content all pitching in and directing our efforts over the following few months.\u003c/p\u003e","title":"A developer team's approach to not having a Product Manager"},{"content":"What is a Gem? Gems are small chunks of code used to perform specific duties without interfering with, or becoming a direct part of our code. The code required for a gem will still be a part of our project’s ecosystem, but we will ultimately remove some overhead, and avoid unnecessary duplication by using a gem instead of coding it ourselves.\nWhy create a Gem? Gems are a helpful byproduct created when you either have a piece of self contained code, or else when a developer realizes the code might benefit others. If we want to use that code elsewhere or want to share it, that’s when it’s time to make a gem.\nHow to create a Gem OK, so we have some standalone code that could help others and that we think works well as a RubyGem. We want to package it up so that it\u0026rsquo;s easy to distribute and install. To do this we need to do three things:\nPut our classes into a namespace to avoid clashing with similarly named classes in other Gems Organise our files into directories following the RubyGems conventions Create the RubyGem and publish it to a public server Namespacing the code Right now, all of our classes and modules would be defined at the top level. As our goal is to distribute this, and the people who install our gem might use it as a library rather than a standalone program, we need to namespace our classes. This prevents naming collisions. For example, if someone sees our gem and likes our User class and wants to use it in their code, they would require it. The problem comes if they already have a User class in their code, it would require both, merging it into a Frankenstein class, and nobody wants that.\nTo avoid this, we need to wrap our classes and specs in a unique namespace / module. As an example of how that would look:\nclass User # ... end Would turn into:\nmodule MyAmazingGem class User # ... end end This would turn the User class into the MyAmazingGem::User class, thus making it different and avoiding confusion. I have used MyAmazingGem as the module name here, but you could use anything.\nIt is always a good idea to run any tests before making any changes. As a note, this blog will assume we are using RSpec to test but some programmers may be using other gems.\nOrganising our files Our next task is to organise our files into directories according to the RubyGems conventions. This way, anyone who installs our gem has a good idea of where to find everything. To do this we create three subdirectories beneath our top level directory:\nbin: Contains the main program file, and any related files lib: Contains all the code for the application (sometimes called ‘library files’) spec: Contains all the RSpec files It is also convention for a gem to contain two top-level plain text documentation files: a README (a short description of the gem and how to use it) and a LICENSE (the license under which you distribute your code - see the MIT license for reference. It doesn\u0026rsquo;t have to be long but try and have one or else people won’t know if they can use it). Remember, after moving all those files around we may have to change our require_relative statements to require. Require then checks for a load path. So the statements may move from: require_relative ‘user’ to require ‘my_amazing_gem/user’.\nBuilding the gem Now we have everything we need to package up our RubyGem, we do that by writing a Gem specification file, or gemspec. The gemspec defines what\u0026rsquo;s in the gem, who made it, the version of the gem, etc. The gemspec will be a file called gem_name.gemspec, the .gemspec part being important, and will be located in the top-level application directory. It will look something like this:\nGem::Specification.new do |s| s.name = \u0026#34;INSERT GEM NAME HERE\u0026#34; s.version = \u0026#34;INSERT VERSION HERE\u0026#34; s.author = \u0026#34;INSERT YOUR NAME HERE\u0026#34; s.email = \u0026#34;INSERT YOUR EMAIL HERE\u0026#34; s.homepage = \u0026#34;INSERT HOMEPAGE URL HERE\u0026#34; s.summary = \u0026#34;INSERT SUMMARY HERE\u0026#34; s.description = File.read(File.join(File.dirname(__FILE__), \u0026#39;README\u0026#39;)) s.licenses = [\u0026#39;MIT\u0026#39;] s.files = Dir[\u0026#34;{bin,lib,spec}/**/*\u0026#34;] + %w(LICENSE README) s.test_files = Dir[\u0026#34;spec/**/*\u0026#34;] s.executables = [ \u0026#39;An array of command-line scripts that RubyGems should install when folks install the gem\u0026#39; ] s.required_ruby_version = \u0026#39;\u0026gt;=1.9\u0026#39; s.add_development_dependency \u0026#39;rspec\u0026#39;, \u0026#39;~\u0026gt; 2.8\u0026#39;, \u0026#39;\u0026gt;= 2.8.0\u0026#39; end name: This field contains the gem name, which should be all lowercase letters with underscores or hyphens separating words. version: This field contains the gem version, which generally follows the major.minor.release numbering notation. For example, \u0026ldquo;1.0.0\u0026rdquo; would indicate the first major release of this gem. author: Your name. email: Your email. homepage: A URL to the gem\u0026rsquo;s homepage. summary: A really short description of the gem. description: A longer description of the gem. licenses: An array of license abbreviations for the gem. files: An array of files to include in the gem package. test_files: An array of just the test (spec) files. executables: An array of command-line scripts that RubyGems should install when someone installs the gem. required_ruby_version: This is really important. Since we used require_relative, which is only supported in Ruby 1.9 or newer, this ensures that the gem can only be run with that version of Ruby. add_development_dependency: Since we use RSpec, it\u0026rsquo;s good to list it as a development dependency. Once the gemspec is all filled in correctly it’s time to build the gem. To do this we need to run: gem build gem_name.gemspec. The build command reads and executes the gemspec, then generates a packaged .gem file. E.g if you named the gem ‘gem_name’ and set the version to 1.0.0, then you’ll end up with a file called: gem_name-1.0.0.gem.\nPublishing the Gem At this point, we could share our packaged .gem file with people by sending it to them or putting it on a shared server. However we could share it with the world on RubyGems.org. First of all, we need to make an account on the site. From here, we need to check there isn\u0026rsquo;t already a gem with the same name (Since RubyGems.org is the main repository for most open-source Ruby code, it\u0026rsquo;s a possibility). Once we are sure the gem name is available we can push the .gem file to the public server by typing: gem push gem_name-1.0.0.gem. If this is the first time you\u0026rsquo;re publishing a gem, you’ll be prompted for your username and password. These credentials are stored for future pushes.\nOnce your gem is pushed it will be available for installation by anyone in the world in just a few seconds. To check and search for it on the public server we can use: gem search -r gem_name. You should see a positive response. To install the gem from the remote server we can use: gem install gem_name.\nWe are done! Congratulations and happy gem building!\n","permalink":"https://engineering.resolvergroup.com/2021/08/creating-a-gem-in-ruby-a-jewelcrafting-guide-for-developers/","summary":"\u003ch2 id=\"what-is-a-gem\"\u003eWhat is a Gem?\u003c/h2\u003e\n\u003cp\u003eGems are small chunks of code used to perform specific duties without interfering with, or becoming a direct part of our code. The code required for a gem will still be a part of our project’s ecosystem, but we will ultimately remove some overhead, and avoid unnecessary duplication by using a gem instead of coding it ourselves.\u003c/p\u003e\n\u003ch2 id=\"why-create-a-gem\"\u003eWhy create a Gem?\u003c/h2\u003e\n\u003cp\u003eGems are a helpful byproduct created when you either have a piece of self contained code, or else when a developer realizes the code might benefit others. If we want to use that code elsewhere or want to share it, that’s when it’s time to make a gem.\u003c/p\u003e","title":"Creating a Gem in Ruby: a Jewelcrafting guide for Developers"},{"content":"Awesomplete is a front-end autocomplete tool that compares a user’s input to a defined data list, and if it recognises the input will make suggestions for the user to choose from.\nWe chose to use the Awesomplete tool in the Accord team while building the journey to enable users to create and submit their dispute. Awesomplete provided a neat solution to our problem of how to let the user choose the organisation they are raising a dispute against. The simplest solution would have been a dropdown list of all available organisations. However, as we will be working with ADR providers representing potentially hundreds or thousands of organisations, a dropdown list would not have been a feasible solution. Enter Awesomplete!\nThe Awesomplete documentation is very comprehensive. It is also easy to get started with the simplest version of an Awesomplete input, with very minimal code:\n\u0026lt;input class=\u0026#34;awesomplete\u0026#34; data-list=\u0026#34;Apples, Bananas, Grapes, Watermelons\u0026#34;\u0026gt; Once the Awesomplete libraries have been linked or added to your project, the only 2 requirements to make an Awesomplete input is the awesomplete CSS class on the input, and a linked data list. There are many ways to link a data list, from simply adding a data-list property, as above, to creating a new Awesomplete object in JavaScript.\nIt is also clear from the documentation how customisable Awesomplete is as a feature, especially when taking advantage of all the JavaScript properties available. You can quickly customise how many characters the user needs to type before Awesomplete will auto-complete, the number of suggested values Awesomplete will display to the user, and how the user can select their chosen suggested value. There are also custom DOM events, prefixed with awesomplete-, so that other actions on the page can be triggered by the user selecting an Awesomplete suggestion, or the Awesomplete pop-up appearing.\nIn the Accord app we have applied our existing CSS styles to the Awesomplete input for seamless styling. The user can either tab or click to select a suggested value. We have used additional Stimulus JavaScript controllers so that once the user’s input matches the data list, the prompt text will disappear and a submit button will appear so that the user can continue with the Create Dispute journey.\nIn the Accord app, we create an Awesomplete JavaScript object in our Stimulus controllers. On the one hand this gives us greater control in customising the input, but on the other hand it does rely on the user’s browser successfully loading JavaScript in order for the Awesomplete input to function. We overcome this challenge by first loading a Rails collection_select form tag. Then, if the JavaScript loads successfully, 3 things happen on page load: Firstly, the Stimulus controller extracts and stores the collection from the collection_select. Secondly, it creates an Awesomplete JavaScript object, using this saved collection as the data list. Finally, it removes the original collection_select from the page, so that only the Awesomplete input remains.\nThis solution presented its own challenge. A collection_select form helper accepts the label displayed to the user (e.g. “Squid Energy PLC”) and the value stored in the database (usually the ID) as separate arguments. An Awesomplete input does not have this distinction - the value it displays to the user is the same as the value it stores in the database on form submission. This is not ideal, as we are either trying to store a company name when the database is expecting an ID number, or we are displaying meaningless ID numbers to the user.\nIn the Accord app we overcome this by including a hidden field in the form. The Stimulus controller compares the user’s input to the stored data list. If it finds a match, it automatically populates the hidden field with the corresponding ID number for that organisation. In this way, the organisation’s label is displayed to the user so that they can easily recognise the organisation they’re raising a dispute against, whereas the organisation’s ID is what is submitted and stored in the database.\nOverall, Awesomplete provided a good solution for our page. It is quick and simple to set up and implement, yet has the potential to be highly customisable and powerful. I hope these tips on how we overcame the challenges along the way are helpful to you in your project!\n","permalink":"https://engineering.resolvergroup.com/2021/08/using-awesomplete-for-auto-complete-inputs/","summary":"\u003cp\u003eAwesomplete is a front-end autocomplete tool that compares a user’s input to a defined data list, and if it recognises the input will make suggestions for the user to choose from.\u003c/p\u003e\n\u003cp\u003eWe chose to use the Awesomplete tool in the Accord team while building the journey to enable users to create and submit their dispute. Awesomplete provided a neat solution to our problem of how to let the user choose the organisation they are raising a dispute against. The simplest solution would have been a dropdown list of all available organisations. However, as we will be working with ADR providers representing potentially hundreds or thousands of organisations, a dropdown list would not have been a feasible solution. Enter Awesomplete!\u003c/p\u003e","title":"Using Awesomplete for auto-complete inputs"},{"content":"VPCs are a way of partitioning infrastructure in AWS to isolate them from communication with other infrastructure. They operate at the network level similar to VLANs and block all external communication by default.\nHowever, sometimes it is necessary to allow some inter-VPC communication. For example, if you have multiple AWS accounts that need to access some shared service, that will require infrastructure in one VPC to access infrastructure in another VPC.\nOn Accord ODR, we want to be able to share anonymised data and statistics with our data team who can analyse it and make it look good for our clients to use. However, they have a separate AWS account containing their data analysis software.\nConnecting VPCs together To allow the data team\u0026rsquo;s AWS account to access this data, we needed to set up a peering connection between our VPC and their VPC. However, our main VPC also contains all our other infrastructure which should not be accessible outside the application itself.\nTo work around this, we set up a separate VPC in our account to hold just a single database that contains the anonymised data. This VPC is peered to our main VPC so an ETL job can regularly take data from our main database, anonymise it and write it to this second database. In turn, this VPC is peered with the data team\u0026rsquo;s VPC, allowing them to access the database only. This setup works because VPC can have multiple peering connections, and also because peering connections are not transitive - i.e. if VPC A is peered with VPC B and VPC B is peered with VPC C, VPC A and VPC C are not peered with each other, even though they both have a common third peer.\nPeering VPCs in the same account First, we setup a peering connection between our main VPC and a second VPC which contains the anonymised database:\nresource \u0026#34;aws_vpc_peering_connection\u0026#34; \u0026#34;vpc_peering_connection\u0026#34; { vpc_id = module.vpc.vpc_id peer_vpc_id = module.peered_vpc.vpc_id auto_accept = true accepter { allow_remote_vpc_dns_resolution = true } requester { allow_remote_vpc_dns_resolution = true } tags = { Name = \u0026#34;production-peering-connection\u0026#34; Environment = var.aws_environment } } We have a Terraform module that sets up our VPCs, so we get the ID of the two VPCs to peer. Since they are both in the same account, we can automatically accept the peering connection. We also enable DNS resolution in both directions so that the peer VPC can access resources in the main VPC by DNS name rather than IP address. Note that this enablement must come after the peering connection has been set up.\nPeering VPCs in separate accounts Now that we have a means of accessing our peer VPC, we need to set up a peering connection between that VPC and the data team\u0026rsquo;s VPC, which is in a separate account (and region).\nresource \u0026#34;aws_vpc_peering_connection\u0026#34; \u0026#34;vpc_peering_connection_data_team\u0026#34; { vpc_id = module.peered_vpc.vpc_id peer_vpc_id = \u0026#34;vpc-0123456789abcdefg\u0026#34; peer_owner_id = \u0026#34;012345678912\u0026#34; peer_region = \u0026#34;eu-west-1\u0026#34; auto_accept = false requester { allow_remote_vpc_dns_resolution = true } tags = { Name = \u0026#34;production-peering-connection-data-team\u0026#34; Environment = var.aws_environment } } Since we\u0026rsquo;re now peering with a VPC in a separate account, we need to provide the account ID and region that the peer VPC exists in. We also cannot automatically accept this connection - the owner of the other account need to do that before the connection is live. Finally, we can only enable DNS resolution on our side (requester) - again, the owner of the other account will also need to enable DNS resolution on their side (accepter).\nSetting up routing We now have peering connections set up, but without routes, there is still no way of directing certain traffic over the peering connection and into the peer VPC. As an example, this is a route set up between our VPC and the data team\u0026rsquo;s VPC:\nresource \u0026#34;aws_route\u0026#34; \u0026#34;vpc_peering_data_team_route_requester_private\u0026#34; { count = length(module.peered_private_subnet.route_table_ids) route_table_id = module.peered_private_subnet.route_table_ids[count.index] destination_cidr_block = \u0026#34;10.0.0.0/16\u0026#34; vpc_peering_connection_id = aws_vpc_peering_connection.vpc_peering_connection_data_team.id } This route says that any traffic with a destination of 10.0.0.0/16 (the CIDR of the data team\u0026rsquo;s VPC) should be routed over the peering connection. Again, there will be an equivalent route on the opposite side to route traffic intended for our VPC\u0026rsquo;s CIDR across the same peering connection.\nWe now have a fully functioning peering connection which allows the data team to access our anonymised database across the peering connection using a DNS name.\n","permalink":"https://engineering.resolvergroup.com/2021/03/setting-up-vpc-peering/","summary":"\u003cp\u003eVPCs are a way of partitioning infrastructure in AWS to isolate them from communication with other infrastructure. They operate at the network level similar to VLANs and block all external communication by default.\u003c/p\u003e\n\u003cp\u003eHowever, sometimes it is necessary to allow some inter-VPC communication. For example, if you have multiple AWS accounts that need to access some shared service, that will require infrastructure in one VPC to access infrastructure in another VPC.\u003c/p\u003e","title":"Setting up VPC peering"},{"content":"Last time, we looked at building a deployment pipeline using CodePipeline.\nGiven the increasing number of countries that are implementing data residency policies, we now need to be able to deploy a copy of our application in a different region along with its database and connected services at the same time as in the main region (London eu-west-2 in this case).\nWe\u0026rsquo;ll look at how we can amend our earlier CodePipeline deployment pipeline to deploy the same application image to ECS in two different regions simultaneously.\nSingle region deployment Last time, our deployment stage for production was the following:\n# Deploy the image to ECS Fargate using a blue/green deployment system (production) stage { name = \u0026#34;Deploy_to_Production\u0026#34; action { name = \u0026#34;Deploy\u0026#34; category = \u0026#34;Deploy\u0026#34; owner = \u0026#34;AWS\u0026#34; provider = \u0026#34;CodeDeployToECS\u0026#34; version = \u0026#34;1\u0026#34; input_artifacts = [\u0026#34;build_output\u0026#34;] configuration = { ApplicationName = aws_codedeploy_app.codedeploy.name DeploymentGroupName = aws_codedeploy_deployment_group.codedeploy_deployment_group_production.deployment_group_name TaskDefinitionTemplateArtifact = \u0026#34;build_output\u0026#34; TaskDefinitionTemplatePath = \u0026#34;taskdef-production.json\u0026#34; AppSpecTemplateArtifact = \u0026#34;build_output\u0026#34; AppSpecTemplatePath = \u0026#34;appspec-production.yaml\u0026#34; } } } This is a simple deployment action that uses a pre-defined CodeDeploy app and deployment group. These are also described in the previous blog post.\nCross-region deployment For a cross-region deployment, we take the above stage as a template and add two extra parameters to end up with:\n# Deploy the image to ECS Fargate using a blue/green deployment system (production) stage { name = \u0026#34;Deploy_to_Production\u0026#34; action { name = \u0026#34;Deploy\u0026#34; category = \u0026#34;Deploy\u0026#34; owner = \u0026#34;AWS\u0026#34; provider = \u0026#34;CodeDeployToECS\u0026#34; version = \u0026#34;1\u0026#34; input_artifacts = [\u0026#34;build_output\u0026#34;] run_order = 1 configuration = { ApplicationName = aws_codedeploy_app.codedeploy.name DeploymentGroupName = aws_codedeploy_deployment_group.codedeploy_deployment_group_production.deployment_group_name TaskDefinitionTemplateArtifact = \u0026#34;build_output\u0026#34; TaskDefinitionTemplatePath = \u0026#34;taskdef-production.json\u0026#34; AppSpecTemplateArtifact = \u0026#34;build_output\u0026#34; AppSpecTemplatePath = \u0026#34;appspec-production.yaml\u0026#34; } } action { name = \u0026#34;Deploy-Singapore\u0026#34; category = \u0026#34;Deploy\u0026#34; owner = \u0026#34;AWS\u0026#34; provider = \u0026#34;CodeDeployToECS\u0026#34; version = \u0026#34;1\u0026#34; input_artifacts = [\u0026#34;build_output\u0026#34;] run_order = 1 region = \u0026#34;ap-southeast-1\u0026#34; configuration = { ApplicationName = aws_codedeploy_app.codedeploy_sg.name DeploymentGroupName = aws_codedeploy_deployment_group.codedeploy_deployment_group_production_sg.deployment_group_name TaskDefinitionTemplateArtifact = \u0026#34;build_output\u0026#34; TaskDefinitionTemplatePath = \u0026#34;taskdef-production-sg.json\u0026#34; AppSpecTemplateArtifact = \u0026#34;build_output\u0026#34; AppSpecTemplatePath = \u0026#34;appspec-production-sg.yaml\u0026#34; } } } Firstly, we now have two actions in the deployment stage. One is our existing action that deploys to eu-west-2, and the second is a similar action that deploys to ap-southeast-1, as defined by the region parameter. It also uses a similar CodeDeploy app and deployment group which point to the ECS infrastructure in ap-southeast-1.\nSecondly, we add a run_order parameter to both actions, and they are both set to 1. This ensures that both deployment actions run concurrently, rather than the second one waiting for the first one to complete successfully.\nApplying this configuration results in the following on the pipeline details page in the AWS Management Console:\nWe have two parallel actions, with the Singapore one displaying a small double-arrow icon denoting an action that is running in a region different to the one that contains the pipeline itself.\nWe can add any number of similar parallel actions deploying to different regions at the same time for a truly global application.\n","permalink":"https://engineering.resolvergroup.com/2021/03/cross-region-deployments-with-codepipeline/","summary":"\u003cp\u003eLast time, we looked at \u003ca href=\"https://engineering.resolvergroup.com/2021/03/triggering-aws-ecs-deployments-via-github-codepipeline-and-ecr/\"\u003ebuilding a deployment pipeline using CodePipeline\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eGiven the increasing number of countries that are implementing data residency policies, we now need to be able to deploy a copy of our application in a different region along with its database and connected services at the same time as in the main region (London \u003ccode\u003eeu-west-2\u003c/code\u003e in this case).\u003c/p\u003e\n\u003cp\u003eWe\u0026rsquo;ll look at how we can amend our earlier CodePipeline deployment pipeline to deploy the same application image to ECS in two different regions simultaneously.\u003c/p\u003e","title":"Cross-region deployments with CodePipeline"},{"content":"Deployments are a key part of running a web application, but most of the time, they are an afterthought when it comes to process and developer ease-of-use.\nHowever, using a number of AWS services, deployments can be made much easier, with most of the process automated.\nOn the Accord ODR project, we use ECS to host our application using Docker containers. Here, I\u0026rsquo;ll show how GitHub, CodePipeline and ECR (Elastic Container Registry) can be used to create an easy-to-use process that mostly just happens in the background and is very light touch.\nCreating a repository for Docker images We start by creating an ECR repository. This is similar to a Docker Hub repository and is a place to upload built Docker images which can then be pulled as part of the deployment process.\nresource \u0026#34;aws_ecr_repository\u0026#34; \u0026#34;app_image_repository\u0026#34; { name = \u0026#34;app-images\u0026#34; image_scanning_configuration { scan_on_push = true } } We create a repository called app-images which we\u0026rsquo;ll use in the deployment process. \u0026ldquo;Scan on push\u0026rdquo; enables a feature which scans for vulnerabilities in your images which are then displayed in the ECR dashboard.\nCodePipeline and related services Now we need to set up CodePipeline. But before we create the pipeline itself, we need to create the components that make it up.\nIn our case, we\u0026rsquo;ll go with a simple pipeline that is triggered by commits being pushed to the master branch, builds a Docker image, runs the test suite, pushes it to ECR then deploys it to ECS.\nBuilding Docker images with CodeBuild CodeBuild can be seen as a script-runner - you provide a shell script that is run and provided with a number of environment variables. The script itself can do pretty much anything. In our case, it will receive the contents of the master branch, build a Docker image, run the automated test suite, then push the image to the ECR repository we created above, ready for deployment.\ndata \u0026#34;template_file\u0026#34; \u0026#34;codebuild_buildspec\u0026#34; { template = file(\u0026#34;templates/buildspec.yaml.tpl\u0026#34;) vars = { aws_account_id = data.aws_caller_identity.current.account_id aws_region = var.aws_region ecr_repository_name = data.terraform_remote_state.infra_ecr_repositories.outputs.app_image_repository_name docker_hub_username = var.docker_hub_username docker_hub_password = var.docker_hub_password build_artifacts_bucket_name = data.terraform_remote_state.infra_build_artifacts_bucket.outputs.build_artifacts_bucket_name } } resource \u0026#34;aws_codebuild_project\u0026#34; \u0026#34;codebuild\u0026#34; { name = \u0026#34;deployment-codebuild-project\u0026#34; service_role = aws_iam_role.codepipeline_role.arn artifacts { type = \u0026#34;CODEPIPELINE\u0026#34; } cache { type = \u0026#34;S3\u0026#34; location = \u0026#34;${data.terraform_remote_state.infra_build_artifacts_bucket.outputs.build_artifacts_bucket_name}/codebuild-cache\u0026#34; } environment { compute_type = \u0026#34;BUILD_GENERAL1_SMALL\u0026#34; image = \u0026#34;${data.aws_caller_identity.current.account_id}.dkr.ecr.${var.aws_region}.amazonaws.com/build-images:latest\u0026#34; type = \u0026#34;LINUX_CONTAINER\u0026#34; image_pull_credentials_type = \u0026#34;CODEBUILD\u0026#34; privileged_mode = true } source { type = \u0026#34;CODEPIPELINE\u0026#34; buildspec = data.template_file.codebuild_buildspec.rendered } } Let\u0026rsquo;s go through what we\u0026rsquo;ve just created. We start with a build spec file (which we\u0026rsquo;ll look at next) that will tell CodeBuild what to do. Next, we create the CodeBuild project itself. We define it to be part of a CodePipeline pipeline (this affects how CodeBuild expects to receive artefacts), we provide an S3 bucket where items can be cached temporarily, then we define where our script will run.\nIn our case, we go with a small general-purpose Linux build machine running a custom-built build image. Our build image is based on the freely-available docker-dind image (available from https://hub.docker.com/_/docker) with a few more tools added in (notably Docker Compose since that\u0026rsquo;s what we use to create our images). This is a Docker image that allows us to build more Docker images inside it by including the Docker server.\nFinally, we include the build spec file. Let\u0026rsquo;s take a look at that.\nversion: 0.2 phases: pre_build: commands: - echo Pre-build started on `date` - echo Setting environment variables for CodeClimate... - export CI_NAME=\u0026#34;AWS CodeBuild\u0026#34; - export CI_BUILD_ID=$CODEBUILD_BUILD_NUMBER - export CI_BUILD_URL=\u0026#34;\u0026lt;https://$\u0026gt;{aws_region}.console.aws.amazon.com/codesuite/codebuild/${aws_account_id}/projects/deployment-codebuild-project/build/$CODEBUILD_BUILD_ID\u0026#34; - export GIT_BRANCH=\u0026#34;master\u0026#34; - export GIT_COMMIT_SHA=`git log -1 --pretty=%H` - export GIT_COMMITTED_AT=`git log -1 --pretty=format:%ct` - export APP_REVISION=`git rev-parse --short HEAD` - echo Authenticating with AWS ECR... - export ECR_DOMAIN=\u0026#34;${aws_account_id}.dkr.ecr.${aws_region}.amazonaws.com\u0026#34; - export ECR_REPO=\u0026#34;$ECR_DOMAIN/${ecr_repository_name}\u0026#34; - export WEB_IMAGE_URI=\u0026#34;$ECR_REPO:$GIT_COMMIT_SHA\u0026#34; - export WEB_IMAGE_URI_LATEST=\u0026#34;$ECR_REPO:latest\u0026#34; - aws ecr get-login-password --region ${aws_region} | docker login --username AWS --password-stdin $ECR_DOMAIN - echo Authenticating with Docker Hub... - docker login --username ${docker_hub_username} --password ${docker_hub_password} - echo Running Docker daemon... - dockerd \u0026amp; - echo Pre-build completed on `date` build: commands: - echo Build started on `date` - echo Creating .env file... - cp env-example .env - echo Building and bringing up Docker containers... - app_revision=$APP_REVISION docker-compose -f docker-compose.aws.yml --log-level WARNING up -d - echo Running test suite... - docker-compose exec -T web .codebuild/run-tests.sh - echo Getting taskdef.json files... - aws s3api get-object --bucket ${build_artifacts_bucket_name} --key defs/staging/task-definition.json taskdef-staging.json - aws s3api get-object --bucket ${build_artifacts_bucket_name} --key defs/production/task-definition.json taskdef-production.json - echo Getting appspec.yaml files... - aws s3api get-object --bucket ${build_artifacts_bucket_name} --key defs/staging/appspec.yaml appspec-staging.yaml - aws s3api get-object --bucket ${build_artifacts_bucket_name} --key defs/production/appspec.yaml appspec-production.yaml - echo Pushing \u0026#34;web\u0026#34; image to ECR... - docker tag web:latest $WEB_IMAGE_URI - docker tag web:latest $WEB_IMAGE_URI_LATEST - docker push $ECR_REPO - echo Building imageDetail.json... - | printf \u0026#34;{\\\\\u0026#34;ImageURI\\\\\u0026#34;: \\\\\u0026#34;%s\\\\\u0026#34;}\u0026#34; \u0026#34;$WEB_IMAGE_URI\u0026#34; \u0026gt; imageDetail.json - echo Build completed on `date` post_build: commands: - echo Post-build started on `date` - echo Saving any screenshots to S3... - mkdir -p tmp/screenshots - docker cp src_web_1:/usr/src/app/tmp/screenshots tmp || true - | if [ \u0026#34;$(ls -A tmp/screenshots)\u0026#34; ]; then aws s3 cp tmp/screenshots s3://${build_artifacts_bucket_name}/screenshots --recursive echo The following screenshots of failed tests are available: for filename in tmp/screenshots/*.*; do basename=`basename $filename` echo \u0026lt;https://$\u0026gt;{build_artifacts_bucket_name}.s3.${aws_region}.amazonaws.com/screenshots/$basename done fi - echo Bringing down Docker containers... - docker-compose down - echo Post-build completed on `date` artifacts: files: - imageDetail.json - taskdef-staging.json - appspec-staging.yaml - taskdef-production.json - appspec-production.yaml There\u0026rsquo;s a lot happening in here, but it can be summarised as follows:\nSet up some environment variables used by CodeClimate (which we\u0026rsquo;re using here for code coverage reporting) Authenticate with ECR and Docker Hub (we\u0026rsquo;ll use both to pull and push Docker images) Run the Docker daemon which we\u0026rsquo;ll use the build the images Create the .env file by copying the example Use Docker Compose to build the Docker images based on our AWS-specific compose file Run the test suite via a script we have in the application repository Get some artefact files from S3 (where we\u0026rsquo;ve uploaded them beforehand) Tag and push the \u0026ldquo;web\u0026rdquo; Docker image (the one that contains our app) to ECR Build an imageDetail.json file which will be used by CodeDeploy to deploy the app Get any screenshots output by RSpec due to failing tests and make them available for viewing more easily Most of this shouldn\u0026rsquo;t be a surprise. Let\u0026rsquo;s take a look at the artefact files we pull from S3. Firstly, the appspec.yaml file:\nversion: 0.0 Resources: - TargetService: Type: \u0026#34;AWS::ECS::Service\u0026#34; Properties: TaskDefinition: \u0026#34;\u0026lt;TASK_DEFINITION\u0026gt;\u0026#34; LoadBalancerInfo: ContainerName: \u0026#34;web\u0026#34; ContainerPort: ${app_port} This is a very simple file that defines for ECR which container, port and task definition it should use to run the app. \u0026lt;TASK_DEFINITION\u0026gt; is an AWS variable that is filled in automatically during deployment.\nNext, the taskdef.json file:\n{ \u0026#34;containerDefinitions\u0026#34;: [ { \u0026#34;environment\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;AWS_REGION\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;${aws_region}\u0026#34; }, { \u0026#34;name\u0026#34;: \u0026#34;PORT\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;${app_port}\u0026#34; }, { \u0026#34;name\u0026#34;: \u0026#34;RAILS_ENV\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;${rails_env}\u0026#34; }, { \u0026#34;name\u0026#34;: \u0026#34;RAILS_MASTER_KEY\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;${rails_master_key}\u0026#34; }, { \u0026#34;name\u0026#34;: \u0026#34;RAILS_SERVE_STATIC_FILES\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;true\u0026#34; }, { \u0026#34;name\u0026#34;: \u0026#34;RAILS_LOG_TO_STDOUT\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;true\u0026#34; }, { \u0026#34;name\u0026#34;: \u0026#34;DATABASE_URL\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;postgresql://${postgresql_username}:${postgresql_password}@${postgresql_host}:5432/${postgresql_db_name}\u0026#34; } ], \u0026#34;essential\u0026#34;: true, \u0026#34;image\u0026#34;: \u0026#34;${app_image}\u0026#34;, \u0026#34;logConfiguration\u0026#34;: { \u0026#34;logDriver\u0026#34;: \u0026#34;awslogs\u0026#34;, \u0026#34;options\u0026#34;: { \u0026#34;awslogs-region\u0026#34;: \u0026#34;${aws_region}\u0026#34;, \u0026#34;awslogs-group\u0026#34;: \u0026#34;ecs-logs\u0026#34;, \u0026#34;awslogs-stream-prefix\u0026#34;: \u0026#34;app-task\u0026#34; } }, \u0026#34;mountPoints\u0026#34;: [], \u0026#34;name\u0026#34;: \u0026#34;web\u0026#34;, \u0026#34;portMappings\u0026#34;: [ { \u0026#34;containerPort\u0026#34;: ${app_port}, \u0026#34;hostPort\u0026#34;: ${app_port}, \u0026#34;protocol\u0026#34;: \u0026#34;tcp\u0026#34; }, { \u0026#34;containerPort\u0026#34;: 22, \u0026#34;hostPort\u0026#34;: 22, \u0026#34;protocol\u0026#34;: \u0026#34;tcp\u0026#34; } ], \u0026#34;volumesFrom\u0026#34;: [] }, { \u0026#34;command\u0026#34;: [ \u0026#34;bundle\u0026#34;, \u0026#34;exec\u0026#34;, \u0026#34;sidekiq\u0026#34; ], \u0026#34;environment\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;AWS_REGION\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;${aws_region}\u0026#34; }, { \u0026#34;name\u0026#34;: \u0026#34;RAILS_ENV\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;${rails_env}\u0026#34; }, { \u0026#34;name\u0026#34;: \u0026#34;RAILS_MASTER_KEY\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;${rails_master_key}\u0026#34; }, { \u0026#34;name\u0026#34;: \u0026#34;DATABASE_URL\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;postgresql://${postgresql_username}:${postgresql_password}@${postgresql_host}:5432/${postgresql_db_name}\u0026#34; } ], \u0026#34;essential\u0026#34;: true, \u0026#34;image\u0026#34;: \u0026#34;${app_image}\u0026#34;, \u0026#34;logConfiguration\u0026#34;: { \u0026#34;logDriver\u0026#34;: \u0026#34;awslogs\u0026#34;, \u0026#34;options\u0026#34;: { \u0026#34;awslogs-region\u0026#34;: \u0026#34;${aws_region}\u0026#34;, \u0026#34;awslogs-group\u0026#34;: \u0026#34;ecs-logs\u0026#34;, \u0026#34;awslogs-stream-prefix\u0026#34;: \u0026#34;sidekiq-task\u0026#34; } }, \u0026#34;mountPoints\u0026#34;: [], \u0026#34;name\u0026#34;: \u0026#34;sidekiq\u0026#34;, \u0026#34;portMappings\u0026#34;: [], \u0026#34;volumesFrom\u0026#34;: [] } ], \u0026#34;cpu\u0026#34;: \u0026#34;${fargate_cpu}\u0026#34;, \u0026#34;executionRoleArn\u0026#34;: \u0026#34;arn:aws:iam::123456789123:role/ecs-task-execution-role\u0026#34;, \u0026#34;taskRoleArn\u0026#34;: \u0026#34;arn:aws:iam::123456789123:role/ecs-task-role\u0026#34;, \u0026#34;family\u0026#34;: \u0026#34;app-task\u0026#34;, \u0026#34;memory\u0026#34;: \u0026#34;${fargate_memory}\u0026#34;, \u0026#34;networkMode\u0026#34;: \u0026#34;awsvpc\u0026#34;, \u0026#34;requiresCompatibilities\u0026#34;: [ \u0026#34;FARGATE\u0026#34; ] } This is the task definition that ECS will run. It defines everything from the environment variables that will be made available to the Docker container to the resources needed and where logs should be saved.\nWe generate these files for each environment and save them to an S3 bucket. The CodeBuild project will then retrieve them and pass them onto the next step of the pipeline so they can be used during the deployment process.\nDeploying images with CodeDeploy Now that we have our application Docker image pushed to ECR, we can deploy it to our environments. This is an example for one environment but multiple CodeDeploy deployment groups can be chained in a pipeline, for example to deploy to staging followed by production.\nresource \u0026#34;aws_codedeploy_app\u0026#34; \u0026#34;codedeploy\u0026#34; { compute_platform = \u0026#34;ECS\u0026#34; name = \u0026#34;codedeploy-app\u0026#34; } resource \u0026#34;aws_codedeploy_deployment_group\u0026#34; \u0026#34;codedeploy_deployment_group_production\u0026#34; { app_name = aws_codedeploy_app.codedeploy.name deployment_group_name = \u0026#34;codedeploy-deployment-group-production\u0026#34; deployment_config_name = \u0026#34;CodeDeployDefault.ECSAllAtOnce\u0026#34; service_role_arn = aws_iam_role.codepipeline_role.arn auto_rollback_configuration { enabled = true events = [\u0026#34;DEPLOYMENT_FAILURE\u0026#34;] } blue_green_deployment_config { deployment_ready_option { action_on_timeout = \u0026#34;CONTINUE_DEPLOYMENT\u0026#34; } terminate_blue_instances_on_deployment_success { action = \u0026#34;TERMINATE\u0026#34; termination_wait_time_in_minutes = 60 } } deployment_style { deployment_option = \u0026#34;WITH_TRAFFIC_CONTROL\u0026#34; deployment_type = \u0026#34;BLUE_GREEN\u0026#34; } ecs_service { cluster_name = \u0026#34;production-app-cluster\u0026#34; service_name = \u0026#34;production-app-service\u0026#34; } load_balancer_info { target_group_pair_info { prod_traffic_route { listener_arns = [data.terraform_remote_state.app_load_balancers_production.outputs.app_https_alb_listener_id] } target_group { name = \u0026#34;production-tg-1\u0026#34; } target_group { name = \u0026#34;production-tg-2\u0026#34; } } } } Here we create a deployment group which determines how CodeDeploy carries out the deployment and where it deploys to.\nWe tell CodeDeploy to deploy to all ECS containers at the same time, using a blue-green strategy. This mean that new containers will be created and bought up, then once they are determined to be ready, we have 1 hour to cancel the deployment before the old containers are removed and the newly-deployed ones take their place. During this time, the old containers are drained and all new connections are routed to the new containers.\nIf there is any failure during the deployment (such as the containers crashing), the deployment is terminated and rolled back to the previous state. In this way, we have zero-downtime deployments and therefore don\u0026rsquo;t need to schedule them out-of-hours. This strategy is enabled by creating two target groups for our load balancer (one for blue, one for green).\nBringing it all together with CodePipeline Now that we have all the pieces, we need to bring them together in a pipeline which we can trigger to carry out the whole process.\nresource \u0026#34;aws_codepipeline\u0026#34; \u0026#34;codepipeline\u0026#34; { name = \u0026#34;codepipeline-pipeline\u0026#34; role_arn = aws_iam_role.codepipeline_role.arn artifact_store { location = data.terraform_remote_state.infra_build_artifacts_bucket.outputs.build_artifacts_bucket_name type = \u0026#34;S3\u0026#34; region = var.aws_region } # Get source code pushed to `master` from GitHub stage { name = \u0026#34;Source\u0026#34; action { name = \u0026#34;Source\u0026#34; category = \u0026#34;Source\u0026#34; owner = \u0026#34;AWS\u0026#34; provider = \u0026#34;CodeStarSourceConnection\u0026#34; version = \u0026#34;1\u0026#34; output_artifacts = [\u0026#34;source_output\u0026#34;] configuration = { # The connection is initially set up via the AWS Management Console ConnectionArn = var.codestar_connection_arn FullRepositoryId = \u0026#34;org/repo\u0026#34; BranchName = \u0026#34;master\u0026#34; OutputArtifactFormat = \u0026#34;CODEBUILD_CLONE_REF\u0026#34; } } } # Build images, run automated test suite and push to ECR stage { name = \u0026#34;Build\u0026#34; action { name = \u0026#34;Build\u0026#34; category = \u0026#34;Build\u0026#34; owner = \u0026#34;AWS\u0026#34; provider = \u0026#34;CodeBuild\u0026#34; version = \u0026#34;1\u0026#34; input_artifacts = [\u0026#34;source_output\u0026#34;] output_artifacts = [\u0026#34;build_output\u0026#34;] configuration = { ProjectName = aws_codebuild_project.codebuild.name } } } # Deploy the image to ECS Fargate using a blue/green deployment system (staging) stage { name = \u0026#34;Deploy_to_Staging\u0026#34; action { name = \u0026#34;Deploy\u0026#34; category = \u0026#34;Deploy\u0026#34; owner = \u0026#34;AWS\u0026#34; provider = \u0026#34;CodeDeployToECS\u0026#34; version = \u0026#34;1\u0026#34; input_artifacts = [\u0026#34;build_output\u0026#34;] configuration = { ApplicationName = aws_codedeploy_app.codedeploy.name DeploymentGroupName = aws_codedeploy_deployment_group.codedeploy_deployment_group_staging.deployment_group_name TaskDefinitionTemplateArtifact = \u0026#34;build_output\u0026#34; TaskDefinitionTemplatePath = \u0026#34;taskdef-staging.json\u0026#34; AppSpecTemplateArtifact = \u0026#34;build_output\u0026#34; AppSpecTemplatePath = \u0026#34;appspec-staging.yaml\u0026#34; } } } # Manually approve deployment to production stage { name = \u0026#34;Approve_Deployment_to_Production\u0026#34; action { name = \u0026#34;Approve\u0026#34; category = \u0026#34;Approval\u0026#34; owner = \u0026#34;AWS\u0026#34; provider = \u0026#34;Manual\u0026#34; version = \u0026#34;1\u0026#34; } } # Deploy the image to ECS Fargate using a blue/green deployment system (production) stage { name = \u0026#34;Deploy_to_Production\u0026#34; action { name = \u0026#34;Deploy\u0026#34; category = \u0026#34;Deploy\u0026#34; owner = \u0026#34;AWS\u0026#34; provider = \u0026#34;CodeDeployToECS\u0026#34; version = \u0026#34;1\u0026#34; input_artifacts = [\u0026#34;build_output\u0026#34;] configuration = { ApplicationName = aws_codedeploy_app.codedeploy.name DeploymentGroupName = aws_codedeploy_deployment_group.codedeploy_deployment_group_production.deployment_group_name TaskDefinitionTemplateArtifact = \u0026#34;build_output\u0026#34; TaskDefinitionTemplatePath = \u0026#34;taskdef-production.json\u0026#34; AppSpecTemplateArtifact = \u0026#34;build_output\u0026#34; AppSpecTemplatePath = \u0026#34;appspec-production.yaml\u0026#34; } } } } Our pipeline starts by connecting to a GitHub repository using a CodeStar connection, which we set up manually beforehand (since it requires authorising AWS to access private GitHub repositories in our case). Any push to the masterbranch triggers the pipeline which gathers a copy of the branch and outputs it for CodeBuild to pick up. CodeBuild runs our previously detailed build spec to build, test and push our Docker image. CodeDeploy then picks this up along with the metadata files and deploys it to ECS in our staging environment.\nAt this point, we have an approval step that means a developer needs to log in to the AWS Management Console and click a button to either approve or reject the progression of the image to our production environment.\nWith this, we now have a fully-featured deployment pipeline that needs only light-touch interaction from a developer to deploy all the way to production.\n","permalink":"https://engineering.resolvergroup.com/2021/03/triggering-aws-ecs-deployments-via-github-codepipeline-and-ecr/","summary":"\u003cp\u003eDeployments are a key part of running a web application, but most of the time, they are an afterthought when it comes to process and developer ease-of-use.\u003c/p\u003e\n\u003cp\u003eHowever, using a number of AWS services, deployments can be made much easier, with most of the process automated.\u003c/p\u003e\n\u003cp\u003eOn the Accord ODR project, we use ECS to host our application using Docker containers. Here, I\u0026rsquo;ll show how GitHub, CodePipeline and ECR (Elastic Container Registry) can be used to create an easy-to-use process that mostly just happens in the background and is very light touch.\u003c/p\u003e","title":"Triggering AWS ECS deployments via GitHub, CodePipeline and ECR"},{"content":"Scheduled jobs (or Cron jobs) are a standard part of web development. Whether you want to update comment counts every night or check for updates once a week, a scheduled job allows you to \u0026ldquo;set and forget\u0026rdquo;.\nThere are a number of methods to implement scheduled jobs. Some rudimentary web apps check the schedule each time someone visits a page. This works as long as there are regular visitors, otherwise the schedule may slip during quiet periods. Better methods include either using the underlying server\u0026rsquo;s cron implementation (if possible) or possibly using a background worker like Sidekiq that also has scheduling capabilities.\nIf your application is hosted on AWS, there is also another way, using a number of native AWS services chained together to make scheduled calls to endpoints in the application, which can then kick-off jobs.\nUsing a Lambda function to call the application The premise of this setup is that the application has one or more endpoints which, when called, kick off background jobs to carry out a particular task.\nWe first start be creating a Lambda function that will call the given endpoint(s) when it itself is triggered on a schedule.\n\u0026#39;use strict\u0026#39;; const https = require(\u0026#39;https\u0026#39;); exports.handler = function(event, context, callback) { const trigger_urls = [ \u0026#39;\u0026lt;https://webhooks.app/hook1\u0026gt;\u0026#39;, \u0026#39;\u0026lt;https://webhooks.app/hook2\u0026gt;\u0026#39; ]; for (let url of trigger_urls) { const request = https.get(url, (res) =\u0026gt; { console.log(\u0026#39;Successfully requested \u0026#39; + url); }); request.on(\u0026#39;error\u0026#39;, (e) =\u0026gt; { callback(\u0026#39;Error requesting \u0026#39; + url + \u0026#39;: \u0026#39; + e.message); }); } }; This is a very simple function that just calls a list of URLs one by one.\nWe could call this function directly from a schedule. However, if there are ever any errors when calling one or more of the URLs, the Lambda function will simply fail and not retry until the next schedule occurs.\nA better way of preventing any transient errors is to build in the concept of retries along with exponential backoff, which ensures we don\u0026rsquo;t swamp the application with multiple requests in a short period of time if the errors continue. Additionally, there should be some way of stopping execution of the function after a number of retries since it\u0026rsquo;s likely the error is not transient.\nWe could build these into the Lambda function, but that would be a lot of work and would reinvent a lot of wheels. Instead, we\u0026rsquo;ll use another AWS service, Step Functions.\nAdding a Step Function to handle errors Step Functions are normally used to chain together a number of Lambda functions and other AWS services into some kind of workflow. However, the feature we\u0026rsquo;ll use here is the built in error handling and retry behaviours with our one Lambda function.\nresource \u0026#34;aws_sfn_state_machine\u0026#34; \u0026#34;background_jobs_step_function\u0026#34; { name = \u0026#34;background_jobs-step-function\u0026#34; role_arn = aws_iam_role.step_function_role.arn definition = data.template_file.background_jobs_step_function_definition_template.rendered } We then create the definition of the state machine (I\u0026rsquo;ve skipped the template file where we provide the ARN of the Lambda function to run):\n{ \u0026#34;Version\u0026#34;: \u0026#34;1.0\u0026#34;, \u0026#34;StartAt\u0026#34;: \u0026#34;RequestState\u0026#34;, \u0026#34;States\u0026#34;: { \u0026#34;RequestState\u0026#34;: { \u0026#34;Type\u0026#34;: \u0026#34;Task\u0026#34;, \u0026#34;Resource\u0026#34;: \u0026#34;${background_jobs_lambda_qualified_arn}\u0026#34;, \u0026#34;End\u0026#34;: true, \u0026#34;Retry\u0026#34;: [ { \u0026#34;ErrorEquals\u0026#34;: [\u0026#34;States.ALL\u0026#34;], \u0026#34;IntervalSeconds\u0026#34;: 5, \u0026#34;BackoffRate\u0026#34;: 5, \u0026#34;MaxAttempts\u0026#34;: 5 } ] } } } The definition has one Lambda function to run, and it will attempt to run it up to 5 times, with a 5 second interval between each run, increasing by 5 seconds each time.\nNow we have our error handling and retries sorted out, we just need to set up a schedule to run the Step Function.\nScheduling everything with EventBridge EventBridge allows us to schedule a service to run in a similar method to a cron job.\nresource \u0026#34;aws_cloudwatch_event_rule\u0026#34; \u0026#34;event_rule_midnight_daily\u0026#34; { name = \u0026#34;eventbridge-rule-midnight-daily\u0026#34; description = \u0026#34;Runs once a day at 12:05am\u0026#34; schedule_expression = \u0026#34;cron(5 0 * * ? *)\u0026#34; } resource \u0026#34;aws_cloudwatch_event_target\u0026#34; \u0026#34;background_jobs_step_function_target\u0026#34; { rule = aws_cloudwatch_event_rule.event_rule_midnight_daily.name arn = data.terraform_remote_state.app_step_functions.outputs.background_jobs_step_function_arn role_arn = aws_iam_role.eventbridge_role.arn } There are two parts to setting up a schedule. First, we have the event rule which defines the actual schedule. In this example, we\u0026rsquo;ll run the Step Function once a day at 12:05 am. Second, we have the event target which defines what EventBridge should run. In this case, we\u0026rsquo;re providing the ARN of the Step Function we set up earlier.\nBringing it all together, we have a combination of EventBridge, Step Functions and Lambda that will make web hook calls to our application on a schedule and handle errors and retries. This is a great example of how a number of AWS services can be chained together to make something new and better.\n","permalink":"https://engineering.resolvergroup.com/2021/02/adding-cron-jobs-with-error-recovery-to-aws-using-lambda-step-functions-and-eventbridge/","summary":"\u003cp\u003eScheduled jobs (or Cron jobs) are a standard part of web development. Whether you want to update comment counts every night or check for updates once a week, a scheduled job allows you to \u0026ldquo;set and forget\u0026rdquo;.\u003c/p\u003e\n\u003cp\u003eThere are a number of methods to implement scheduled jobs. Some rudimentary web apps check the schedule each time someone visits a page. This works as long as there are regular visitors, otherwise the schedule may slip during quiet periods. Better methods include either using the underlying server\u0026rsquo;s \u003ccode\u003ecron\u003c/code\u003e implementation (if possible) or possibly using a background worker like Sidekiq that also has scheduling capabilities.\u003c/p\u003e","title":"Adding cron jobs with error recovery to AWS using Lambda, Step Functions and EventBridge"},{"content":"Security Groups are a best practice feature of VPCs in AWS that act similar to a firewall. They allow access to various resources such as EC2 instances, load balancers or RDS databases to be controlled to other resources or a set of IP addresses.\nFor example, you may set up an EC2 instance to only be accessible by a load balancer. This means uses cannot bypass the load balancer to access your instance directly. You might set this up as follows:\nresource \u0026#34;aws_security_group\u0026#34; \u0026#34;app\u0026#34; { name = \u0026#34;app-access\u0026#34; vpc_id = data.terraform_remote_state.app_vpc.outputs.vpc_id description = \u0026#34;Controls access to the app containers\u0026#34; ingress { protocol = \u0026#34;tcp\u0026#34; from_port = 8080 to_port = 8080 security_groups = [aws_security_group.load_balancer.id] } egress { protocol = \u0026#34;-1\u0026#34; from_port = 0 to_port = 0 cidr_blocks = [\u0026#34;0.0.0.0/0\u0026#34;] } } Here, we set up the Security Group that\u0026rsquo;s attached to the load balancer to access the application (in this instance, running in ECS). We allow the app containers ingress access only from the load balancer to port 8080 (where the app is running). We also allow the app container all egress access back to the Internet.\nThis setup works in all instances where you can specify access to a resource using another Security Group, but what if you want to limit access to CloudFront, which doesn\u0026rsquo;t have a Security Group?\nIn this case, you would normally fall back to using IP addresses. For example, the following example allows SSH access to a bastion EC2 instance:\nresource \u0026#34;aws_security_group\u0026#34; \u0026#34;bastion\u0026#34; { name = \u0026#34;bastion-access\u0026#34; vpc_id = data.terraform_remote_state.app_vpc.outputs.vpc_id description = \u0026#34;Controls access to the bastion host\u0026#34; ingress { protocol = \u0026#34;tcp\u0026#34; from_port = 22 to_port = 22 cidr_blocks = [\u0026#34;0.0.0.0/0\u0026#34;] } egress { protocol = \u0026#34;-1\u0026#34; from_port = 0 to_port = 0 cidr_blocks = [\u0026#34;0.0.0.0/0\u0026#34;] } } Here, we allow access from any IP address to port 22 for SSH, and again we allow all egress access back to the Internet.\nThe difference with CloudFront is that the IP addresses it uses for ingress access change over time, so we can\u0026rsquo;t just hardcode a list of them into the configuration. Instead, we need to get the list dynamically whenever we run Terraform, and pass this list to the security group.\n# Get the current list of AWS CloudFront IP ranges data \u0026#34;aws_ip_ranges\u0026#34; \u0026#34;cloudfront\u0026#34; { services = [\u0026#34;cloudfront\u0026#34;] } # Chunk the CloudFront IP ranges into blocks of 30 to get around security group limits locals { cloudfront_ip_ranges_chunks = chunklist(data.aws_ip_ranges.cloudfront.cidr_blocks, 30) } We use the aws_ip_ranges data provider and ask it for the IP addresses currently associated with CloudFront. This will be the correct list for the moment we run Terraform. Before passing this list on to a Security Group, however, we need to split it into chunks of 30 IP addresses. This is because Security Groups have a hard upper limit of 30 IP addresses.\nNow we have the IP addresses, we can create a Security Group to grant them access to the load balancer as follows:\nresource \u0026#34;aws_security_group\u0026#34; \u0026#34;load_balancer\u0026#34; { count = length(local.cloudfront_ip_ranges_chunks) name = \u0026#34;load-balancer-access${count.index == 0 ? \u0026#34;\u0026#34; : count.index + 1}\u0026#34; vpc_id = data.terraform_remote_state.app_vpc.outputs.vpc_id description = \u0026#34;Controls access to the ALB\u0026#34; ingress { protocol = \u0026#34;tcp\u0026#34; from_port = 443 to_port = 443 cidr_blocks = local.cloudfront_ip_ranges_chunks[count.index] } ingress { protocol = \u0026#34;tcp\u0026#34; from_port = 8080 to_port = 8080 cidr_blocks = local.cloudfront_ip_ranges_chunks[count.index] } egress { protocol = \u0026#34;-1\u0026#34; from_port = 0 to_port = 0 cidr_blocks = [\u0026#34;0.0.0.0/0\u0026#34;] } } Here we create a Security Group per chunk, and feed the chunk into an ingress rule. As of this blog post, this will result in 4 Security Groups being created, each of which allows access from 30 CloudFront IP addresses to the load balancer.\nPlease note, however, that CloudFront IP addresses change every now and then. At the moment, there doesn\u0026rsquo;t seem to be a better way of restricting access than running this configuration every now and then to refresh the list of IP addresses allowed access.\n","permalink":"https://engineering.resolvergroup.com/2021/02/allowing-cloudfront-to-access-load-balancers-through-security-groups/","summary":"\u003cp\u003eSecurity Groups are a best practice feature of VPCs in AWS that act similar to a firewall. They allow access to various resources such as EC2 instances, load balancers or RDS databases to be controlled to other resources or a set of IP addresses.\u003c/p\u003e\n\u003cp\u003eFor example, you may set up an EC2 instance to only be accessible by a load balancer. This means uses cannot bypass the load balancer to access your instance directly. You might set this up as follows:\u003c/p\u003e","title":"Allowing CloudFront to access load balancers through Security Groups"},{"content":"I recently started working on a ticket where I was asked to add a new mail template and the logic behind it. I have not really worked with either Mandrill or Mailchimp much before, so I started doing research into how to do this. Every blog post and Stack Overflow post that I could find is aimed at doing this for the first time, i.e. integrating a Ruby on Rails app to use Mandrill/Mailchimp. After looking at documentation and reading through our codebase I managed to work it out in the end, but I hope that this post will explain to any other developer how to do this off the bat.\nThis is a screenshot of the email that I was provided with (on Mandrill) and asked to implement. The Mandrill interface is very user friendly, and you can click onto any of the images/text and change them if needed.\nThe important thing to note here is the merge tags, which are ways to display information dynamically in emails, such as *|FIRST_NAME|* and *|COMPANY_NAME|* shown here. (I’ll come back to these later) but other than that it is essentially a self-explanatory email.\nSo essentially, we want to send an email to users who have closed their case file the previous day without leaving feedback already. In the email, it should contain the button ‘Leave feedback’ which when clicked, takes them to the page to leave feedback. Simple!\nMy first step was to create a new mailer which lives in app/mailers/user_mailer.rb as it will be sent to users.\ndef feedback_email(user_id, case_file_id) case_file = CaseFile.find(case_file_id) user = User.find(user_id) global_merge_vars = { \u0026#39;RES_NUMBER\u0026#39; =\u0026gt; case_file.resolver_ref, \u0026#39;COMPANY_NAME\u0026#39; =\u0026gt; case_file.company.name, \u0026#39;FIRST_NAME\u0026#39; =\u0026gt; user.first_name, \u0026#39;FEEDBACK_URL\u0026#39; =\u0026gt; new_dashboard_case_file_feedback_url(case_file.id, locale: user.link_locale) }.map { |k, v| { name: k, content: v } } end The two parameters are user_id and case_file_id as we need both of those to locate the relevant user and case file. The global_merge_vars are related to the email template and the merge tags that we saw previously. For instance, here the ‘FIRST_NAME’ from the global_merge_vars gets used on the template to display the first name, in the example case, ‘Katie’. You can use these to dynamically display any required information. FEEDBACK_URL is used to provide the link in the button for ‘Leave feedback’ from the email.\nMy next step was to add a query to find relevant case files. In this case, I need to find case files that were closed yesterday (0:00-23:59) that don’t already have feedback.\nIn app/models/case_file.rb:\nscope :prompt_for_feedback, lambda { case_files = CaseFile.where(closed_at: 1.days.ago.beginning_of_day..1.days.ago.end_of_day) case_files = case_files.all { |cf| cf.feedback.nil? } } Next, we need to add a new service that deals with the sending of the email.\nIn app/services/feedback_reminder.rb:\nclass FeedbackReminder def run CaseFile.prompt_for_feedback.find_each(batch_size: 100) do |case_file| UserMailer.feedback_email(case_file.user_id, case_file.id).deliver_later(wait_until: 7.hours.from_now) rescue StandardError =\u0026gt; e case_file.add_event( description: \u0026#34;[Error] #{e.message}\u0026#34;, source: \u0026#39;web\u0026#39;, category: \u0026#39;email.feedback_email\u0026#39; ) end end end I chose to do this using batches (batch_size: 100) due to the high number of case files that this method will be handling.\nUsing deliver_later enqueues the emails using Active Job to be sent later. When they do get sent, they get sent using deliver_now. I’ll come back to the 7.hours.from_now part!\nNext, I added the text for the subject inside of a locale.\nIn config/locales/mailers/en-GB.yml:\nfeedback_email: subject: \u0026#34;How did your case go?\u0026#34; Next, to add the rake schedule. We use rake as our scheduler, it is what runs the job behind the scenes.\nIn config/schedule.rb:\nevery :day, at: \u0026#39;5.00am\u0026#39; do rake \u0026#39;scheduler:send_feedback_reminder_email\u0026#39; end I chose to run this job at 5:00am as it is a time of low traffic for us. Because this job will handle thousands of emails each day, it is important to pick a time of low traffic so that the server can handle it efficiently. This is why in app/services/feedback_reminder.rb I use:\ndeliver_later(wait_until: 7.hours.from_now) deliver_later(wait_until: 7.hours.from_now)\nAs I want the email to be delivered to inboxes around midday so that people will be on lunch break and (hopefully!) more likely to read the email and to engage with it. Doing it this way, there’s no need to worry about traffic to the site as the job will already be enqueued for sending.\nNext I added the task to the scheduler which calls the process.\nIn lib/tasks/scheduler.rake:\ndesc \u0026#39;This task sends a prompt to leave feedback\u0026#39; task send_feedback_reminder_email: %i[environment logger] do FeedbackReminder.new.run end Finally, my specs. In spec/mailer/user_mailer_spec.rb:\ndescribe \u0026#39;#feedback_email\u0026#39; do let(:message) { UserMailer.feedback_email(user.id, case_file.id).message } subject { described_class.new } let(:case_file) { create(:case_file, :closed_yesterday, user: user) } it \u0026#39;sets from\u0026#39; do expect(message.from).to eq([\u0026#39;no-reply@example.com\u0026#39;]) end it \u0026#39;sets to\u0026#39; do expect(message.to).to eq([user.email]) end it \u0026#39;sets subject\u0026#39; do expect(message.subject).to eq(\u0026#39;How did your case go?\u0026#39;) end it \u0026#39;sets template\u0026#39; do expect(message[:template].value).to eq \u0026#39;en-gb-uk-feedback-followup\u0026#39; end it \u0026#39;sets global merge vars\u0026#39; do expect(message[:global_merge_vars].instance_variable_get(:@unparsed_value)).to include( { name: \u0026#39;FIRST_NAME\u0026#39;, content: user.first_name }, { name: \u0026#39;COMPANY_NAME\u0026#39;, content: case_file.company.name }, { name: \u0026#39;RES_NUMBER\u0026#39;, content: case_file.resolver_ref }, name: \u0026#39;FEEDBACK_URL\u0026#39;, content: \u0026#34;http://complaint.example.com/dashboard/case_files/#{case_file.id}/feedbacks/new\u0026#34; ) end end And spec/services/feedback_reminder_spec.rb:\nrequire \u0026#39;rails_helper\u0026#39; RSpec.describe FeedbackReminder do before { create(:case_file, :closed_yesterday) } describe \u0026#39;run\u0026#39; do it \u0026#39;sends an email\u0026#39; do expect { FeedbackReminder.new.run }.to change { enqueued_jobs.count }.by(1) expect(enqueued_jobs.last[:args]).to include(\u0026#39;UserMailer\u0026#39;, \u0026#39;feedback_email\u0026#39;) end end end One thing to take note of is that if you make any changes to the template on Mailchimp then once you are finished, make sure to select ‘Send to Mandrill’ from the dropdown menu.\n","permalink":"https://engineering.resolvergroup.com/2021/01/sending-emails-using-mandrill-mailchimp-with-ruby-on-rails/","summary":"\u003cp\u003eI recently started working on a ticket where I was asked to add a new mail template and the logic behind it. I have not really worked with either Mandrill or Mailchimp much before, so I started doing research into how to do this. Every blog post and Stack Overflow post that I could find is aimed at doing this for the first time, i.e. integrating a Ruby on Rails app to use Mandrill/Mailchimp. After looking at documentation and reading through our codebase I managed to work it out in the end, but I hope that this post will explain to any other developer how to do this off the bat.\u003c/p\u003e","title":"Sending emails using Mandrill/Mailchimp with Ruby on Rails"},{"content":"One of the best ways of working out who is how many people are visiting your site, see patterns of traffic and analyse them over time is to look at your access logs.\nMost web servers collect some form of access logs, mostly in a standard format such as the Common Log Format. As a CDN, CloudFront can also collect access logs when configured to do so, storing them in an S3 bucket ready for analysis.\nresource \u0026#34;aws_s3_bucket\u0026#34; \u0026#34;cloudfront_logs_bucket\u0026#34; { bucket = \u0026#34;cloudfront-logs\u0026#34; lifecycle_rule { id = \u0026#34;cloudfront_logs_lifecycle_rule\u0026#34; enabled = true expiration { days = 365 } } } resource \u0026#34;aws_cloudfront_distribution\u0026#34; \u0026#34;cloudfront_cdn\u0026#34; { # ... logging_config { bucket = aws_s3_bucket.cloudfront_logs_bucket.bucket_domain_name prefix = \u0026#34;main/\u0026#34; } # ... } Here, we set up CloudFront to save access logs to an S3 bucket, which we create with a lifecycle rule to make sure we\u0026rsquo;re not keeping logs forever.\nQuerying logs using SQL The beauty of Athena is that it allows you to query any text files in S3 buckets using SQL. A perfect use for Athena is to pipe our access logs into it, and since we have a large volume of data, querying it using a familiar language built for those volumes is perfect.\nFirst, we need to set Athena up:\nresource \u0026#34;aws_s3_bucket\u0026#34; \u0026#34;cloudfront_logs_athena_results_bucket\u0026#34; { bucket = \u0026#34;cloudfront-logs-athena-results\u0026#34; lifecycle_rule { id = \u0026#34;cloudfront_logs_athena_results_lifecycle_rule\u0026#34; enabled = true expiration { days = 365 } } } resource \u0026#34;aws_athena_database\u0026#34; \u0026#34;cloudfront_logs_athena_database\u0026#34; { name = \u0026#34;cloudfront_logs\u0026#34; bucket = aws_s3_bucket.cloudfront_logs_athena_results_bucket.bucket } resource \u0026#34;aws_athena_workgroup\u0026#34; \u0026#34;cloudfront_logs_athena_workgroup\u0026#34; { name = \u0026#34;cloudfront_logs_workgroup\u0026#34; description = \u0026#34;Workgroup for Athena queries on CloudFront access logs\u0026#34; configuration { enforce_workgroup_configuration = true publish_cloudwatch_metrics_enabled = true result_configuration { output_location = \u0026#34;s3://${aws_s3_bucket.cloudfront_logs_athena_results_bucket.bucket}/output/\u0026#34; encryption_configuration { encryption_option = \u0026#34;SSE_S3\u0026#34; } } } } We set up another S3 bucket which is used to cache the results of Athena queries. This makes them a lot faster, and importantly, means you don\u0026rsquo;t need to pay for the same query over and over again.\nWe then set up the Athena database and a workgroup, which is a way of separating different workloads and data sets.\nSo far, so good, but we still don\u0026rsquo;t have any data in Athena. We now need to create a table in the database that will contain the access log data.\nresource \u0026#34;null_resource\u0026#34; \u0026#34;cloudfront_logs_athena_table\u0026#34; { triggers = { athena_database = aws_athena_database.cloudfront_logs_athena_database.id athena_results_bucket = aws_s3_bucket.cloudfront_logs_athena_results_bucket.id } provisioner \u0026#34;local-exec\u0026#34; { environment = { AWS_REGION = eu-west-2 } command = \u0026lt;\u0026lt;-EOF aws athena start-query-execution --query-string file://templates/create_table_main.sql --output json --query-execution-context Database=${self.triggers.athena_database} --result-configuration OutputLocation=s3://${self.triggers.athena_results_bucket} EOF } provisioner \u0026#34;local-exec\u0026#34; { when = destroy command = \u0026lt;\u0026lt;-EOF aws athena start-query-execution --query-string \u0026#39;DROP TABLE IF EXISTS cloudfront_logs.cloudfront_logs\u0026#39; --output json --query-execution-context Database=${self.triggers.athena_database} --result-configuration OutputLocation=s3://${self.triggers.athena_results_bucket} EOF } } Terraform does not currently support creation of Athena tables out-of-the-box, so we have to use the null resource with the local-exec provisioner, which is a way of executing arbitrary commands based on triggers. Here, we run the AWS CLI to execute a query which creates the table. We set up two triggers to execute this command when they change, and we also define a separate command to run on destroy which drops the table to clean up after itself.\nThe table creation is done with an SQL query in create_table_main.sql:\nCREATE EXTERNAL TABLE IF NOT EXISTS cloudfront_logs.cloudfront_logs ( `date` DATE, time STRING, location STRING, bytes BIGINT, request_ip STRING, method STRING, host STRING, uri STRING, status INT, referrer STRING, user_agent STRING, query_string STRING, cookie STRING, result_type STRING, request_id STRING, host_header STRING, request_protocol STRING, request_bytes BIGINT, time_taken FLOAT, xforwarded_for STRING, ssl_protocol STRING, ssl_cipher STRING, response_result_type STRING, http_version STRING, fle_status STRING, fle_encrypted_fields INT, c_port INT, time_to_first_byte FLOAT, x_edge_detailed_result_type STRING, sc_content_type STRING, sc_content_len BIGINT, sc_range_start BIGINT, sc_range_end BIGINT ) ROW FORMAT DELIMITED FIELDS TERMINATED BY \u0026#39;\\\\t\u0026#39; LOCATION \u0026#39;s3://cloudfront-logs/main/\u0026#39; TBLPROPERTIES (\u0026#39;skip.header.line.count\u0026#39; = \u0026#39;2\u0026#39;); The fields are all the ones available in the CloudFront logs. We set the source location to the S3 bucket where CloudFront stores the logs, and ignore the header rows.\nWe now finally have all the bits we need to be able to query our logs. So we just need to go to the Athena console and run a sample query:\nHere we\u0026rsquo;ve run a simple query to get the first 10 log entries. You can see that it took about 1.5 seconds and scanned about 1.67MB of data. These statistics are used to calculate how much you pay for the query.\nNow everything\u0026rsquo;s set up, and we can run queries of any complexity to answer a particular question about visitors to our app quickly and easily.\n","permalink":"https://engineering.resolvergroup.com/2020/12/setting-up-athena-to-analyse-cloudfront-access-logs/","summary":"\u003cp\u003eOne of the best ways of working out who is how many people are visiting your site, see patterns of traffic and analyse them over time is to look at your access logs.\u003c/p\u003e\n\u003cp\u003eMost web servers collect some form of access logs, mostly in a standard format such as the \u003ca href=\"https://en.wikipedia.org/wiki/Common_Log_Format\"\u003eCommon Log Format\u003c/a\u003e. As a CDN, CloudFront can also collect access logs when configured to do so, storing them in an S3 bucket ready for analysis.\u003c/p\u003e","title":"Setting up Athena to analyse CloudFront access logs"},{"content":"Remember remember, the 5th November…the Resolver developers took part in another AWS GameDay! Our team of 4 had a wide range of engineering, AWS, and most importantly, GameDay experience. Given that the Resolver team were prize-winners at the last GameDay in July, the pressure was on to keep up our reputation.\nLike the last time, the premise of the day was that our team works for a fictitious start-up, ‘Unicorn Rentals’. However this time, the theme and focus of the day was on security. Each team would have to complete various tasks to win points, and the teams with the most points win prizes. There were noticeably more teams taking part this time - from just 6 in July to now 33! It was going to be tough to keep our prize-winning reputation.\nAt the start of the day, everyone logged in to receive our briefing from our AWS hosts. We were greeted with an SMS from the CEO of Unicorn Rentals, who was livid about being contacted while on holiday about multiple security issues in the company - isn’t this what we’d been hired for? We’d best get it sorted, a-sap.\nEveryone then separated into our team Chime chats, and logged into our team dashboard to see what tasks awaited us (and where we could keep an eye on the all-important rankings and scoreboard.)\nWell, it’s safe to say that Unicorn Rentals was not super secure, and would probably have incurred some serious GDPR fines. Our AWS account was riddled with security issues, from unencrypted databases, to passwords and credentials being reused, to EC2 instances being exposed to vulnerabilities, and even a suspicious previous employee who had been leaking our data to external markets!\nAWS GameDays are suitable for anyone to take part, regardless of your AWS experience. Although it obviously helps if you know what you’re doing, two members of our team had little to no experience and were still able to get stuck in and start fixing some of the issues. Between our shared knowledge, the guidance from the task, and the recommended reading sent round before the event, we were able to effectively use the Secrets Manager service to start rotating credentials, Amazon Inspector to identify which of our EC2 instances had been compromised and needed rebuilding, IAM Access Analyzer to find suspicious “Uncle Danny” and shut down his account, and we all cheered when we finally stopped losing points due to unencrypted database snapshots! The more experienced members were even able to set up a Lambda function to automate some of the processes, for extra points.\nLearning from the team’s experience last time, we decided to divide and conquer and tackle a task each. There were multiple “checkpoints” for each task, each worth more precious points, so there was plenty to keep us busy! But if any of us got stuck, or if we were waiting for instances to build or databases to copy, we were all on hand in the Chime chat to share screens, pair up or offer advice.\nIn the end, we came 16th out of the 33 teams. So no prizes this time, but only 2,000 points from top place, and there were only 100 points in it between the top 6 teams. So it was a tight battle, and we placed respectably.\nThe best thing about the GameDay for me was that it was a great opportunity to actually use the AWS services, without worrying about breaking anything or exposing your environment. There is so much fantastic training and documentation available on AWS, but the GameDay just gives you so much more understanding and context for what services to use, how they work, and what they’re capable of. I learnt so much from the GameDay, and feel so much more confident in using AWS now. And had a lot of fun in doing it!\n","permalink":"https://engineering.resolvergroup.com/2020/11/aws-security-gameday/","summary":"\u003cp\u003eRemember remember, the 5th November…the Resolver developers took part in another AWS GameDay! Our team of 4 had a wide range of engineering, AWS, and most importantly, GameDay experience. Given that the Resolver team were prize-winners at \u003ca href=\"https://engineering.resolvergroup.com/2020/07/aws-gameday-2020/\"\u003ethe last GameDay in July\u003c/a\u003e, the pressure was on to keep up our reputation.\u003c/p\u003e\n\u003cp\u003eLike the last time, the premise of the day was that our team works for a fictitious start-up, ‘Unicorn Rentals’. However this time, the theme and focus of the day was on security. Each team would have to complete various tasks to win points, and the teams with the most points win prizes. There were noticeably more teams taking part this time - from just 6 in July to now 33! It was going to be tough to keep our prize-winning reputation.\u003c/p\u003e","title":"AWS Security GameDay"},{"content":"If you have an app that accepts file uploads, then either you\u0026rsquo;re aware of the potential risks, or you should be.\nWith Accord ODR, we started by limiting the types of files that can be uploaded by users. This is the easy part: disallow executable files or anything else that can be scripted. This includes not only obvious candidates like JavaScript files but also things like WMF images, since these can contain executable code.\nThat\u0026rsquo;s all well and good, but file format detection can absolutely be circumvented, and so even when we get an allowed file format uploaded, we have to quarantine it until it\u0026rsquo;s been scanned for viruses, and always assume that the file is infected until it has been proven to be clean.\nOf course, virus scanning is not a panacea and files can (and do) get through a scan while also containing a virus. There\u0026rsquo;s always the chance that we\u0026rsquo;ll allow another user to download a file from the site that contains a virus even though our scan has marked it as clean. However, by isolating these files in S3, they at least cannot infect the app itself and potentially obtain access to personal data.\nUsing S3 VirusScan The most popular solution currently available is Widdix\u0026rsquo;s AWS S3 VirusScan, available from the AWS Marketplace. This is basically a wrapper around ClamAV, an open source virus scanner. The wrapper includes a Lambda function that is triggered each time a file is uploaded to a watched S3 bucket, which then runs ClamAV against the file, and adds metadata to the file to tag its scan result. It also sends a notification to SNS which can be used to trigger other actions (more about that below).\nWe use Rails\u0026rsquo; built-in Active Storage to upload files to the watched S3 bucket which then triggers a virus scan.\nGetting virus scan results into your app Since the virus scanning happens in AWS, the app has no idea of the current scan status of a given uploaded file.\nLuckily for us, S3 VirusScan comes with an SNS topic where notifications are posted each time a file is scanned. The notification contains the name of the file and the scanned status (clean or infected).\nUsing an SNS topic subscription, we can subscribe an endpoint in our app to receive these notifications:\nresource \u0026#34;aws_sns_topic_subscription\u0026#34; \u0026#34;app_s3_virusscan_subscription\u0026#34; { topic_arn = aws_cloudformation_stack.s3_virusscan.outputs.FindingsTopicArn protocol = \u0026#34;https\u0026#34; endpoint = var.app_s3_virusscan_notification_endpoint endpoint_auto_confirms = true } The app_s3_virusscan_notification_endpoint variable is the full URL to an HTTP endpoint that will be called each time a notification is available (for example, https://webhooks.example.com/s3-virusscan).\nSetting up the endpoint controller At our endpoint, we have a controller than handles incoming SNS notifications. Looking closely, the endpoint_auto_confirms flag is set to true, and this means that when the subscription is first created, the endpoint must also be able to return a confirmation message, otherwise the subscription will fail.\nWith that caveat out of the way, let\u0026rsquo;s take a look at our controller:\ndef s3_virus_scan head 400 unless subscription_confirmation_message? || notification_message? raw_message_body = request.body.read message_body = JSON.parse(raw_message_body) verify_message_authenticity!(raw_message_body) confirm_sns_subscription(message_body) if subscription_confirmation_message? update_virus_scan_status_for_blob(message_body) if notification_message? rescue JSON::ParserError head 400 end This is the method that we have linked to our endpoint route and therefore is called for every notification.\nHere, we check the notification is either a subscription confirmation or an actual virus scan notification, then we parse the JSON body and either confirm the subscription, or update our own internal state with the virus scan result for the scanned file.\nHelpfully for us, SNS notifications are sent with a X-AMS-SNS-Message-Type HTTP header that allows us to easily determine what we have:\ndef subscription_confirmation_message? request.headers[\u0026#34;x-amz-sns-message-type\u0026#34;] == \u0026#34;SubscriptionConfirmation\u0026#34; end def notification_message? request.headers[\u0026#34;x-amz-sns-message-type\u0026#34;] == \u0026#34;Notification\u0026#34; end With that done, we next need to parse the body to extract the information we need. To make sure we aren\u0026rsquo;t being fooled by someone sending us a fake notification to mark a virus-laden file as clean, we run a method provided by the aws-sdk-sns gem which verifies signatures on the notification to make sure it\u0026rsquo;s legitimate:\ndef verify_message_authenticity!(message) verifier = Aws::SNS::MessageVerifier.new head 401 and return unless verifier.authentic?(message) end Now that we know we have a legitimate notification, we need to process it. If the notification is a subscription confirmation, it means we\u0026rsquo;re setting up the subscription for the first time. We just need to respond to signal to AWS that the subscription can be set up:\ndef confirm_sns_subscription(message) head 400 and return unless message[\u0026#34;SubscribeURL\u0026#34;].present? # Send an HTTP GET request to the given URL to confirm the subscription url = URI.parse(message[\u0026#34;SubscribeURL\u0026#34;]) Net::HTTP.start(url.host, url.port, use_ssl: true) do |http| request = Net::HTTP::Get.new(url) http.read_timeout = 5 http.max_retries = 0 http.request(request) end render json: { confirmed: true }.to_json, status: 200 rescue Errno::EADDRNOTAVAIL, Net::ReadTimeout head 400 end A subscription confirmation comes with a SubscribeURL field. We make a request to that URL, which confirms the subscription.\nIf, on the other hand, the notification is about a virus scan result, then we need to update our records for the given file to mark it as clean or infected:\nVIRUS_SCAN_STATUS_MAPPING = { \u0026#34;no\u0026#34; =\u0026gt; :no_scan, \u0026#34;clean\u0026#34; =\u0026gt; :clean, \u0026#34;infected\u0026#34; =\u0026gt; :infected }.freeze def update_virus_scan_status_for_blob(message) key = message[\u0026#34;MessageAttributes\u0026#34;][\u0026#34;key\u0026#34;][\u0026#34;Value\u0026#34;] status = VIRUS_SCAN_STATUS_MAPPING[message[\u0026#34;MessageAttributes\u0026#34;][\u0026#34;status\u0026#34;][\u0026#34;Value\u0026#34;]] blob = ActiveStorage::Blob.find_by(key: key) blob\u0026amp;.update(virus_scan_status: status) render json: { key: key, status: status }.to_json, status: 200 end Here, we find the Active Storage blob based on the file name, and we update its virus scan status accordingly.\nThe virus_scan_status attribute for a blob is a custom one added by a simple migration:\nclass AddVirusScanStatusToActiveStorageBlobs \u0026lt; ActiveRecord::Migration[6.0] def change add_column :active_storage_blobs, :virus_scan_status, :integer, default: 0, null: false end end We then set this field up as an enum on blobs (this is an initialiser that is run when the app starts):\nmodule ActiveStorageBlobVirusScanStatus extend ActiveSupport::Concern included do enum virus_scan_status: %i[no_scan clean infected] end end Rails.configuration.to_prepare do ActiveStorage::Blob.include(ActiveStorageBlobVirusScanStatus) end \u0026ldquo;Quarantining\u0026rdquo; infected files If the virus scanner marks a file as infected, then we want to make sure that file cannot be downloaded by app users. We could do this by immediately deleting the file, but there is a chance the status is a false positive (all virus scanner vendors sometimes mess up their signature files), or we may be interested in analysing the file further.\nFor these reasons, we keep infected files in our S3 bucket, but we prevent them from being downloaded using an S3 bucket policy:\n{ \u0026#34;Version\u0026#34;: \u0026#34;2012-10-17\u0026#34;, \u0026#34;Statement\u0026#34;: [ { \u0026#34;Effect\u0026#34;: \u0026#34;Allow\u0026#34;, \u0026#34;Action\u0026#34;: [ \u0026#34;s3:GetObject\u0026#34; ], \u0026#34;Principal\u0026#34;: { \u0026#34;AWS\u0026#34;: \u0026#34;${cloudfront_oai_iam_arn}\u0026#34; }, \u0026#34;Resource\u0026#34;: [ \u0026#34;${assets_s3_bucket}/*\u0026#34; ], \u0026#34;Condition\u0026#34;: { \u0026#34;StringEquals\u0026#34;: { \u0026#34;s3:ExistingObjectTag/s3-virusscan\u0026#34;: \u0026#34;clean\u0026#34; } } } ] } In a previous blog post, we discussed setting up an S3 bucket policy for our assets bucket (where we store uploaded files) to allow CloudFront to serve these files, authenticating itself with an origin access identity (OAI).\nWe now add a condition to that policy, which only allows access by CloudFront if the virus scan status of a file is \u0026ldquo;clean\u0026rdquo;. This means that S3 will deny access to any files that have either not yet been scanned, or have been marked as infected.\nThis is a nice failsafe to ensure that even if the app attempts to construct a link to such a file and serve it, policy will block that file from actually being served.\nDoing something with the status Now that everything is set up, every uploaded file starts with a no_scan status. Once the file is scanned and we\u0026rsquo;ve been notified of the result, we can take various actions.\nIn our app, we display a message if the file has not yet been scanned or if it has been marked as infected. This gives feedback to the user about the status of their uploads and allows them to take action, for example to re-upload a file or get in touch with us if they suspect a false positive.\nPeriodic re-scanning Right now, this setup scans each file once when it has been first uploaded. This is fine for the majority of cases, but there may be situations where a very new virus which is not yet detectable by virus scanners has infected a file, and that file is marked as clean.\nThe best way around this issue is to periodically re-scan all the files in our S3 bucket and mark any that are now found to be infected. This could be done, for example, with an EventBridge schedule rule which triggers the virus scanner.\nHowever, the issue with this approach is that as the number of files in the bucket increases, the scan time will also increase, which increases costs in Lambda run time. It also increases costs in repeatedly retrieving files from the bucket and then writing their status metadata back.\nThis is an area we are still investigating, and hopefully in the future we\u0026rsquo;ll be able to come up with a solution that balances the requirement to detect infected files with the requirement to not add extra unnecessary infrastructure cost.\n","permalink":"https://engineering.resolvergroup.com/2020/11/virus-scanning-files-in-s3-and-integrating-with-rails/","summary":"\u003cp\u003eIf you have an app that accepts file uploads, then either you\u0026rsquo;re aware of the potential risks, or you should be.\u003c/p\u003e\n\u003cp\u003eWith Accord ODR, we started by limiting the types of files that can be uploaded by users. This is the easy part: disallow executable files or anything else that can be scripted. This includes not only obvious candidates like JavaScript files but also things like \u003ca href=\"https://en.wikipedia.org/wiki/Windows_Metafile_vulnerability\"\u003eWMF images\u003c/a\u003e, since these can contain executable code.\u003c/p\u003e","title":"Virus scanning files in S3 and integrating with Rails"},{"content":"VCR is a gem that is super helpful if your Ruby application is using an external API. When testing, if an actual API is called, this will result in slow test performance. Although a single test may not seem that slow when run individually, the impact of using many API calls in tests will significantly slow down the time it takes to run tests.\nThis is why VCR is so handy! What it allows you to do is to record a ‘cassette’ of your API request and will save the results and then use the cassette in testing from then on. This means that once set up, the first time that the test is run, VCR uses the actual API and records the actual response in a (pretend!) ‘cassette’, this saved response is then used in place of using the actual API each time the test is run. The benefits of this is that now your tests will run faster, but also that the tests will not be dependent on the external service working.\nHow to install and use VCR:\nAdd VCR gem to your gemfile: gem \u0026#39;vcr\u0026#39; And run:\nbundle install Create spec/support/vcr_setup.rb and use the following code: VCR.configure do |c| # The directory where your cassettes will be saved c.cassette_library_dir = \u0026#39;spec/vcr\u0026#39; # Your HTTP request service. You can use whichever is your preference. c.hook_into :webhook end Now that setup is complete, it’s time to put VCR into action and test out the API call in question: # spec/api/widgets_spec.rb RSpec.describe \u0026#39;API::Widgets\u0026#39;, vcr: true do # Any tests making external requests in here will now use vcr end The first time this test is run, VCR will create a new cassette and store it in the corresponding location, so in this instance spec/vcr/api/widgets_spec.yaml.\nAnd that’s it! That is the basic setup and usage of VCR. There are plenty of configuration options available, such as setting a timeframe in which it should automatically re-write the cassette (and making a new API call). Do check the documentation.\n","permalink":"https://engineering.resolvergroup.com/2020/11/using-vcr-in-tests/","summary":"\u003cp\u003e\u003ca href=\"https://github.com/vcr/vcr\"\u003eVCR\u003c/a\u003e is a gem that is super helpful if your Ruby application is using an external API. When testing, if an actual API is called, this will result in slow test performance. Although a single test may not seem \u003cem\u003ethat\u003c/em\u003e slow when run individually, the impact of using many API calls in tests will significantly slow down the time it takes to run tests.\u003c/p\u003e\n\u003cp\u003eThis is why VCR is so handy! What it allows you to do is to record a ‘cassette’ of your API request and will save the results and then use the cassette in testing from then on. This means that once set up, the first time that the test is run, VCR uses the actual API and records the actual response in a (pretend!) ‘cassette’, this saved response is then used in place of using the actual API each time the test is run. The benefits of this is that now your tests will run faster, but also that the tests will not be dependent on the external service working.\u003c/p\u003e","title":"Using VCR in tests"},{"content":"Last time, we mentioned CI as one of things that we try to automate as much as possible. This time, we\u0026rsquo;ll go through more details about our CI setup and how it relates to our deployment pipeline.\nSetting up the CodeBuild project As well as being used as part of a CodePipeline, CodeBuild can also be used on its own as a script-runner of sorts. This makes it ideal as somewhere to run automated tests, and this is exactly what we do.\ndata \u0026#34;template_file\u0026#34; \u0026#34;codebuild_buildspec\u0026#34; { template = file(\u0026#34;templates/buildspec.yaml.tpl\u0026#34;) vars = { aws_account_id = data.aws_caller_identity.current.account_id aws_region = var.aws_region build_artifacts_bucket_name = data.terraform_remote_state.infra_build_artifacts_bucket.outputs.build_artifacts_bucket_name } } resource \u0026#34;aws_codebuild_project\u0026#34; \u0026#34;codebuild\u0026#34; { name = \u0026#34;ci-codebuild-project\u0026#34; service_role = aws_iam_role.codebuild_role.arn artifacts { type = \u0026#34;NO_ARTIFACTS\u0026#34; } cache { type = \u0026#34;LOCAL\u0026#34; modes = [\u0026#34;LOCAL_DOCKER_LAYER_CACHE\u0026#34;] } environment { compute_type = \u0026#34;BUILD_GENERAL1_SMALL\u0026#34; image = \u0026#34;org/docker-19.03-dind:latest\u0026#34; type = \u0026#34;LINUX_CONTAINER\u0026#34; image_pull_credentials_type = \u0026#34;CODEBUILD\u0026#34; privileged_mode = true } source { type = \u0026#34;GITHUB\u0026#34; location = \u0026#34;\u0026lt;https://github.com/org/repo\u0026gt;\u0026#34; buildspec = data.template_file.codebuild_buildspec.rendered } } This is a similar setup to our one-off Fargate containers for running database migrations, which also runs in CodeBuild.\nWe get the CodeBuild spec (which we\u0026rsquo;ll go into next), and then set up the project. The project setup itself is very simple - we just give CodeBuild our GitHub repository, and tell it to run our project inside the docker-19.03-dind Docker container.\nThe docker-19.03-dind Docker container is one we build and run ourselves, and it\u0026rsquo;s hosted on Docker Hub for anyone to use. It\u0026rsquo;s based on the docker-19.03-dind base container, and we add Docker Compose, Bash, Git and the AWS CLI. This means we can use our Docker Compose setup for CI easily to bring up all the relevant containers. DIND here stands for \u0026ldquo;Docker in Docker\u0026rdquo;, which means the container itself also runs Docker. This is required for Docker Compose to work.\nThe CodeBuild buildspec Our buildspec is a little complex but does only a few things:\nversion: 0.2 phases: pre_build: commands: - echo Pre-build started on `date` - echo Setting environment variables for CodeClimate... - export CI_NAME=\u0026#34;AWS CodeBuild\u0026#34; - export CI_BUILD_ID=$CODEBUILD_BUILD_NUMBER - export CI_BUILD_URL=\u0026#34;\u0026lt;https://$\u0026gt;{aws_region}.console.aws.amazon.com/codesuite/codebuild/${aws_account_id}/projects/ci-codebuild-project/build/$CODEBUILD_BUILD_ID\u0026#34; - export GIT_BRANCH=$CODEBUILD_WEBHOOK_HEAD_REF - export GIT_COMMIT_SHA=`git log -1 --pretty=%H` - export GIT_COMMITTED_AT=`git log -1 --pretty=format:%ct` - echo Running Docker daemon... - dockerd \u0026amp; - echo Pre-build completed on `date` build: commands: - echo Build started on `date` - echo Creating .env file... - cp env-example .env - echo Building and bringing up Docker containers... - docker-compose -f docker-compose.aws.yml --log-level WARNING up -d - echo Running test suite... - docker-compose exec -T web .codebuild/run-tests.sh - echo Build completed on `date` post_build: commands: - echo Post-build started on `date` - echo Saving any screenshots to S3... - mkdir -p tmp/screenshots - docker cp app_web_1:/usr/src/app/tmp/screenshots tmp || true - | if [ \u0026#34;$(ls -A tmp/screenshots)\u0026#34; ]; then aws s3 cp tmp/screenshots s3://${build_artifacts_bucket_name}/screenshots --recursive echo The following screenshots of failed tests are available: for filename in tmp/screenshots/*.*; do basename=`basename $filename` echo \u0026lt;https://$\u0026gt;{build_artifacts_bucket_name}.s3.${aws_region}.amazonaws.com/screenshots/$basename done fi - echo Bringing down Docker containers... - docker-compose down - echo Post-build completed on `date` Pre-build The pre-build section sets up some environment variables (some of them by running git). These are used by the Code Climate test reporter, which reports things like test coverage on every run to our Code Climate account.\nIt also starts the Docker daemon in the background.\nBuild We start by setting up the .env file from our sample, and then run Docker Compose, which brings up all the containers we need to be able to run the automated test suite.\nFinally, we run a script in our app repo named run-tests.sh which runs the automated test suite.\nPost-build Our automated test suite generates screenshots for every failed test, and here we copy those from the temporary test container to an S3 bucket which is then accessible to our developers. We also print out details of this bucket and links to the screenshots to make life easier.\nFinally, we tear down all the containers and complete the build.\nThe test runner The run-tests.sh file is a simple shell script:\n#!/bin/sh set -e # Install and start Code Climate test reporter wget -q -O cc-test-reporter \u0026lt;https://s3.amazonaws.com/codeclimate/test-reporter/test-reporter-latest-linux-amd64\u0026gt; chmod +x cc-test-reporter ./cc-test-reporter before-build # Create the database and load the schema bundle exec rake db:create db:schema:load # Run the test suite bundle exec rspec # Report test code coverage to Code Climate ./cc-test-reporter format-coverage -t simplecov -o coverage/codeclimate.json coverage/.resultset.json ./cc-test-reporter upload-coverage Apart from setting up the database and running the test suite, we also set up the Code Climate test reporter, and then upload the test coverage output we get from simplecov. This allows us to track test coverage in our apps over time.\nIntegrating with GitHub Now, none of this is really useful unless we can integrate it with GitHub, our SCM system.\nLuckily for us, CodeBuild has a built-in webhook system and it knows how to integrate this with GitHub.\nresource \u0026#34;aws_codebuild_webhook\u0026#34; \u0026#34;codebuild_webhook\u0026#34; { project_name = aws_codebuild_project.codebuild.name filter_group { filter { type = \u0026#34;EVENT\u0026#34; pattern = \u0026#34;PUSH\u0026#34; } filter { type = \u0026#34;HEAD_REF\u0026#34; pattern = \u0026#34;master\u0026#34; exclude_matched_pattern = true } } } Here, we set up a webhook that is triggered on every push to our repo, apart from pushes to master. This means that we can run our deployment pipeline (which also runs our automated test suite) on pushes to master without also running this project.\nOnce the webhook is set up, not only will every push trigger the project, but the results will also be reported back to the commit/PR. Any exit from the buildspec with an error code other than zero will report back a failure. The result also links back to the project output for easier debugging.\n","permalink":"https://engineering.resolvergroup.com/2020/10/ci-using-github-and-aws-codebuild/","summary":"\u003cp\u003e\u003ca href=\"https://engineering.resolvergroup.com/2020/09/sending-slack-alerts-to-approve-codepipeline-deployments/\"\u003eLast time\u003c/a\u003e, we mentioned CI as one of things that we try to automate as much as possible. This time, we\u0026rsquo;ll go through more details about our CI setup and how it relates to our deployment pipeline.\u003c/p\u003e\n\u003ch2 id=\"setting-up-the-codebuild-project\"\u003eSetting up the CodeBuild project\u003c/h2\u003e\n\u003cp\u003eAs well as being used as part of a CodePipeline, CodeBuild can also be used on its own as a script-runner of sorts. This makes it ideal as somewhere to run automated tests, and this is exactly what we do.\u003c/p\u003e","title":"CI using GitHub and AWS CodeBuild"},{"content":"At Resolver, we aim to automate as much of the boring technical jobs as possible, and that includes the whole continuous integration (CI) and deployment pipeline. We trigger these from GitHub, so a mere push or merge can kick-off our automated test suite or deploy to our staging environment for more automated and manual testing.\nOur deployment pipeline also happens to deploy the same artefacts that have been deployed to our staging environment to our production environment. The end goal is to be able to do this automatically, but for that to be viable, we need to have 100% trust in our testing strategy, and as with most organisations, we\u0026rsquo;re not there yet. Therefore, for the time being, we want to be able to pause the pipeline after the staging deployment, and manually approve it to continue to production.\nWith AWS CodePipeline, this step is easy. The product supports a manual approval stage, and we insert that into our pipleline as follows:\nstage { name = \u0026#34;Approve_Deployment_to_Production\u0026#34; action { name = \u0026#34;Approve\u0026#34; category = \u0026#34;Approval\u0026#34; owner = \u0026#34;AWS\u0026#34; provider = \u0026#34;Manual\u0026#34; version = \u0026#34;1\u0026#34; configuration = { NotificationArn = aws_sns_topic.codepipeline_manual_approval_alerts_sns_topic.arn CustomData = \u0026#34;Approval needed for production deployment\u0026#34; } } } Most of the stage is pretty basic, but note the configuration block at the bottom. It refers to an SNS topic and a custom string. That\u0026rsquo;s where our custom alerts come into play.\nFinding out when a deployment is pending The downside of this automated deployment pipeline is that developers aren\u0026rsquo;t always aware of what\u0026rsquo;s happening in the background. One or more deployment may have occurred to the staging environment and may be pending a production deployment. How can we ensure developers are aware of this so deployments don\u0026rsquo;t get delayed?\nThis is where SNS comes to the rescue. SNS is a service that receives notifications from various sources and can carry out an action on each receipt. The approval CodePipeline stage supports notifying an SNS topic (a way of separating different notification types and their appropriate actions) each time the stage is triggered. SNS in turn supports triggering a Lambda function each time a notification is received. Putting those features together, we can come up with a solution that posts alerts to a predefined Slack channel each time a production deployment is pending. That way, everyone can see this and act on it.\nCreating a Lambda function We start by defining a Lambda function that will send a message to a given Slack channel:\nresource \u0026#34;aws_lambda_function\u0026#34; \u0026#34;send_codepipeline_manual_approval_alerts_to_slack_lambda\u0026#34; { filename = \u0026#34;packages/send_codepipeline_manual_approval_alerts_to_slack.zip\u0026#34; function_name = \u0026#34;codepipeline_to_slack-lambda\u0026#34; handler = \u0026#34;send_codepipeline_manual_approval_alerts_to_slack.handler\u0026#34; role = aws_iam_role.lambda_role.arn description = \u0026#34;Send CodePipeline manual approval alerts to Slack\u0026#34; runtime = \u0026#34;nodejs12.x\u0026#34; source_code_hash = filebase64sha256(\u0026#34;packages/send_codepipeline_manual_approval_alerts_to_slack.zip\u0026#34;) } This Terraform configuration takes a Lambda function that has been compressed into a Zip file. The Zip file contains a single file with the function, in any of the languages supported by Lambda. Here, we\u0026rsquo;ve defined NodeJS 12.x as the runtime and our function will be written in JavaScript as a result.\n\u0026#39;use strict\u0026#39;; var https = require(\u0026#39;https\u0026#39;); var util = require(\u0026#39;util\u0026#39;); exports.handler = function(event, context) { // Log what has been received and decode the JSON message console.log(\u0026#39;Message received from SNS:\u0026#39;, JSON.stringify(event, null, 2)); var message = JSON.parse(event.Records[0].Sns.Message); // Construct message to send to Slack var postData = { \u0026#34;channel\u0026#34;: \u0026#34;#my-slack-channel\u0026#34;, \u0026#34;username\u0026#34;: \u0026#34;CodePipeline\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;*\u0026#34; + message.approval.customData + \u0026#34;*\u0026#34;, \u0026#34;icon_emoji\u0026#34;: \u0026#34;:aws:\u0026#34; }; postData.attachments = [ { \u0026#34;text\u0026#34;: \u0026#34;Please visit \u0026#34; + message.approval.approvalReviewLink + \u0026#34; to approve or reject this deployment.\u0026#34; } ]; var options = { method: \u0026#39;POST\u0026#39;, hostname: \u0026#39;hooks.slack.com\u0026#39;, port: 443, path: \u0026#39;/services/XXX/YYY/ZZZ\u0026#39; }; // Send message to Slack var req = https.request(options, function(res) { res.setEncoding(\u0026#39;utf8\u0026#39;); res.on(\u0026#39;data\u0026#39;, function(chunk) { context.done(null); }); }); req.on(\u0026#39;error\u0026#39;, function(e) { console.log(\u0026#39;Problem with request: \u0026#39; + e.message); }); req.write(util.format(\u0026#34;%j\u0026#34;, postData)); req.end(); }; This function takes the message it receives from SNS and extracts some data, which it then assembles into an API call to send to a Slack endpoint URL. To get your unique URL, add a new incoming webhook in Slack.\nCreate this function in a file named the same as the Zip file that you\u0026rsquo;ll compress it into.\nAlso, remember to create the Lambda IAM role. This role will allow your Lambda function to log its output to CloudWatch, which is invaluable if it fails!\nSetting up SNS Now we have our CodePipeline and Lambda function, we need an SNS topic to link them together.\nresource \u0026#34;aws_sns_topic\u0026#34; \u0026#34;codepipeline_manual_approval_alerts_sns_topic\u0026#34; { name = \u0026#34;codepipeline-manual-approval-alerts-sns-topic\u0026#34; display_name = \u0026#34;CodePipeline manual approval alerts\u0026#34; lambda_failure_feedback_role_arn = aws_iam_role.sns_role.arn } resource \u0026#34;aws_sns_topic_subscription\u0026#34; \u0026#34;codepipeline_manual_approval_alerts_sns_topic_subscription\u0026#34; { topic_arn = aws_sns_topic.codepipeline_manual_approval_alerts_sns_topic.arn protocol = \u0026#34;lambda\u0026#34; endpoint = aws_lambda_function.send_codepipeline_manual_approval_alerts_to_slack_lambda.arn } resource \u0026#34;aws_lambda_permission\u0026#34; \u0026#34;slack_alerts_lambda_allow_invocation_from_sns\u0026#34; { action = \u0026#34;lambda:InvokeFunction\u0026#34; function_name = aws_lambda_function.send_codepipeline_manual_approval_alerts_to_slack_lambda.function_name principal = \u0026#34;sns.amazonaws.com\u0026#34; source_arn = aws_sns_topic.codepipeline_manual_approval_alerts_sns_topic.arn } Here, we create an SNS topic, we link it to the Lambda function using a \u0026ldquo;subscription\u0026rdquo;, and we give the Lambda function the permission to be called from the SNS topic.\nThe SNS role is similar to the Lambda role and just allows it to write logs to CloudWatch.\nSeeing it in action To test this out, kick-off your CodePipeline and wait for it to reach the manual approval step. If everything\u0026rsquo;s worked as intended, you should see a message like the one below show up in the Slack channel you defined in the Lambda function earlier:\nIf you don\u0026rsquo;t see the message above, both Lambda and SNS send their logs to CloudWatch, so check there for any tell-tale failure messages. These are commonly to do with missing permissions or incorrect Slack API configuration.\n","permalink":"https://engineering.resolvergroup.com/2020/09/sending-slack-alerts-to-approve-codepipeline-deployments/","summary":"\u003cp\u003eAt Resolver, we aim to automate as much of the boring technical jobs as possible, and that includes the whole continuous integration (CI) and deployment pipeline. We trigger these from GitHub, so a mere push or merge can kick-off our automated test suite or deploy to our staging environment for more automated and manual testing.\u003c/p\u003e\n\u003cp\u003eOur deployment pipeline also happens to deploy the same artefacts that have been deployed to our staging environment to our production environment. The end goal is to be able to do this automatically, but for that to be viable, we need to have 100% trust in our testing strategy, and as with most organisations, we\u0026rsquo;re not there yet. Therefore, for the time being, we want to be able to pause the pipeline after the staging deployment, and manually approve it to continue to production.\u003c/p\u003e","title":"Sending Slack alerts to approve CodePipeline deployments"},{"content":"One of the tenets of modern framework-based web development is the management of your database state in code. Rather like infrastructure-as-code, it means there is an immutable history of the state of the application database at any point in time, which can be re-applied or rolled back as necessary. In addition, the desired state of the database and the application are tied together at any point in time to reduce the chances of divergence.\nPart and parcel of having database migrations is being able to run them at the correct time during the deployment process. With classic deployment methods, this may just be a case of adding a migration command just before or after the deployment command.\nHowever, if you\u0026rsquo;re using CodeDeploy with CodePipeline for your deployment, you\u0026rsquo;ll have noticed that there\u0026rsquo;s no built-in functionality for database migrations as part of deployment. That\u0026rsquo;s where the flexibility of CodePipeline comes into play.\nCodePipeline to the rescue CodePipeline by definition exists to bring together disparate parts of SCM, build and deployment into a single place. In practice, this flexibility means you\u0026rsquo;re free to add as many steps as you need in order to complete a deployment, in whichever order makes sense.\nNormally, a minimal pipeline consists of a SCM source, a CodeBuild project that builds one or more artefacts, and a CodeDeploy project that deploys those artefacts to an AWS service (such as EC2 or ECS).\nGiven the flexibility of pipelines, and the fact that CodeBuild is basically a script runner, it\u0026rsquo;s trivial to add another CodeBuild project in the appropriate place that runs database migrations just before a CodeDeploy project deploys your code to staging or production.\nOn our project, we have added a second CodeBuild project with a very small script that runs a single command to migrate the database to the latest version.\nA one-off task for running migrations Since we\u0026rsquo;re deploying to ECS using Fargate, we can make use of ECS task definitions to define a new task that will run our migrations for us. The reason for this is that the task will have all the context and access it requires to run the script, along with the latest version of the code that is about to be deployed.\nWe start by defining the task:\ndata \u0026#34;template_file\u0026#34; \u0026#34;app_db_migration_container_definition\u0026#34; { template = file(\u0026#34;templates/db-migration-container-definition.json.tpl\u0026#34;) vars = { aws_region = var.aws_region aws_environment = var.aws_environment app_image = var.app_image app_port = var.app_port fargate_cpu = var.fargate_cpu fargate_memory = var.fargate_memory postgresql_host = var.postgresql_host postgresql_username = var.postgresql_username postgresql_password = data.aws_secretsmanager_secret_version.postgresql-primary_password.secret_string postgresql_db_name = var.postgresql_db_name redis_domain = var.redis_domain rails_master_key = var.rails_master_key } } data \u0026#34;template_file\u0026#34; \u0026#34;app_db_migration_task_definition\u0026#34; { template = file(\u0026#34;templates/db-migration-task-definition.json.tpl\u0026#34;) vars = { container_definition = data.template_file.app_db_migration_container_definition.rendered aws_environment = var.aws_environment fargate_cpu = var.fargate_cpu fargate_memory = var.fargate_memory } } resource \u0026#34;aws_ecs_task_definition\u0026#34; \u0026#34;app_db_migration_task_definition\u0026#34; { family = \u0026#34;staging-app-db-migration-task\u0026#34; execution_role_arn = aws_iam_role.ecs_task_execution_role.arn task_role_arn = aws_iam_role.ecs_task_role.arn network_mode = \u0026#34;awsvpc\u0026#34; requires_compatibilities = [\u0026#34;FARGATE\u0026#34;] cpu = var.fargate_cpu memory = var.fargate_memory container_definitions = data.template_file.app_db_migration_container_definition.rendered } We have two data sources that define the contents of the task definition. Then we have the task definition itself that uses this data along with IAM roles to create an ECS task that we can run on demand.\nThe important thing to note here is that while this task definition is very similar to the one we use to run the actual app containers, this one is not part of an overarching ECS service. That means that it will not be run automatically, or restarted if it stops for any reason. This is desirable since we\u0026rsquo;re essentially using it in a throwaway fashion.\nThe actual content of the task definition should be very similar to whatever you have for your app containers, with one difference - the container definition will most probably have to override the default Docker command that you\u0026rsquo;ve defined in your Dockerfile:\n\u0026#34;command\u0026#34;: [ \u0026#34;bin/rails\u0026#34;, \u0026#34;db:migrate\u0026#34; ] This changes the default command to one that runs the database migrations. Once they\u0026rsquo;re run, the command will exit and will in turn bring the container down with it.\nA CodeBuild project to run the one-off task Now we have the task definition in AWS, we can go ahead and create a CodeBuild project to make use of it:\ndata \u0026#34;template_file\u0026#34; \u0026#34;codebuild_db_migration_buildspec_staging\u0026#34; { template = file(\u0026#34;templates/buildspec-db-migration.yaml.tpl\u0026#34;) vars = { ecs_cluster = \u0026#34;staging-app-cluster\u0026#34; ecs_task_definition = \u0026#34;staging-app-db-migration-task\u0026#34; ecs_subnet_ids = join(\u0026#34;,\u0026#34;, data.terraform_remote_state.app_vpc_staging.outputs.private_subnet_ids) ecs_security_groups = join(\u0026#34;,\u0026#34;, data.terraform_remote_state.app_security_groups_staging.outputs.sg_app_ids) } } resource \u0026#34;aws_codebuild_project\u0026#34; \u0026#34;codebuild_db_migration_staging\u0026#34; { name = \u0026#34;deployment-db-migration-codebuild-project-staging\u0026#34; service_role = aws_iam_role.codepipeline_role.arn artifacts { type = \u0026#34;CODEPIPELINE\u0026#34; } cache { type = \u0026#34;S3\u0026#34; location = \u0026#34;${data.terraform_remote_state.infra_build_artifacts_bucket.outputs.build_artifacts_bucket_name}/codebuild-cache\u0026#34; } environment { compute_type = \u0026#34;BUILD_GENERAL1_SMALL\u0026#34; image = \u0026#34;accordodr/docker-19.03-dind:latest\u0026#34; type = \u0026#34;LINUX_CONTAINER\u0026#34; image_pull_credentials_type = \u0026#34;CODEBUILD\u0026#34; privileged_mode = true } source { type = \u0026#34;CODEPIPELINE\u0026#34; buildspec = data.template_file.codebuild_db_migration_buildspec_staging.rendered } } Here we create the CodeBuild project. We give it details of the ECS cluster and task definition to run as well as some network details that will be important later on.\nThe buildspec The buildspec for the above CodeBuild project itself is:\nversion: 0.2 phases: build: commands: - echo Build started on `date` - echo Running database migrations... - aws ecs run-task --launch-type FARGATE --cluster ${ecs_cluster} --task-definition ${ecs_task_definition} --network-configuration \u0026#34;awsvpcConfiguration={subnets=[${ecs_subnet_ids}],securityGroups=[${ecs_security_groups}]}\u0026#34; - echo Build completed on `date` As you can see, the buildspec is very simple. It just runs an AWS CLI command that kicks off the task definition we created above. For the Fargate launch type, a list of the VPC subnets and security groups connected with the ECS cluster need to be provided.\nAdding this all to the pipeline Now we have the task definition and a CodeBuild project that runs it, we just need to tie it all together by adding that project to our pipeline:\nresource \u0026#34;aws_codepipeline\u0026#34; \u0026#34;codepipeline\u0026#34; { name = \u0026#34;infra-codepipeline-pipeline\u0026#34; ... # Build images, run automated test suite and push to ECR stage { name = \u0026#34;Build\u0026#34; action { name = \u0026#34;Build\u0026#34; category = \u0026#34;Build\u0026#34; owner = \u0026#34;AWS\u0026#34; provider = \u0026#34;CodeBuild\u0026#34; version = \u0026#34;1\u0026#34; input_artifacts = [\u0026#34;source_output\u0026#34;] output_artifacts = [\u0026#34;build_output\u0026#34;] configuration = { ProjectName = aws_codebuild_project.codebuild.name } } } # Run database migrations (staging) stage { name = \u0026#34;Run_Database_Migrations_Staging\u0026#34; action { name = \u0026#34;Migrate\u0026#34; category = \u0026#34;Build\u0026#34; owner = \u0026#34;AWS\u0026#34; provider = \u0026#34;CodeBuild\u0026#34; version = \u0026#34;1\u0026#34; input_artifacts = [\u0026#34;build_output\u0026#34;] output_artifacts = [] configuration = { ProjectName = aws_codebuild_project.codebuild_db_migration_staging.name } } } # Deploy the image to ECS Fargate using a blue/green deployment system (staging) stage { name = \u0026#34;Deploy_to_Staging\u0026#34; action { name = \u0026#34;Deploy\u0026#34; category = \u0026#34;Deploy\u0026#34; owner = \u0026#34;AWS\u0026#34; provider = \u0026#34;CodeDeployToECS\u0026#34; version = \u0026#34;1\u0026#34; input_artifacts = [\u0026#34;build_output\u0026#34;] configuration = { ApplicationName = aws_codedeploy_app.codedeploy.name DeploymentGroupName = aws_codedeploy_deployment_group.codedeploy_deployment_group_staging.deployment_group_name TaskDefinitionTemplateArtifact = \u0026#34;build_output\u0026#34; TaskDefinitionTemplatePath = \u0026#34;taskdef-staging.json\u0026#34; AppSpecTemplateArtifact = \u0026#34;build_output\u0026#34; AppSpecTemplatePath = \u0026#34;appspec-staging.yaml\u0026#34; } } } } Above is a sample pipeline that shows two CodeBuild projects - one that build the artefacts and one that runs the database migrations, followed by a CodeDeploy project that deploys the artefacts built earlier in the pipeline.\nWith this setup, you can always be sure that the database migrations have run before your application is deployed. If the migrations fail for any reason, the command will exit with a non-zero code, which will in turn exit the container with an error. CodePipeline is able to pick this up and stop the pipeline run with an error message.\nWhat to remember when creating database migrations You may have noticed that we run the database migrations before we deploy a new version of our application. We do this because the application will expect new or changed columns to be available when it starts running, and we don\u0026rsquo;t want a gap between the application being deployed and the migrations completing.\nThe astute amongst you will note, however, that this creates another issue - the database will change while the current version of the application is still running. In addition, if the deployment fails, this will become a permanent situation since the migrations are not rolled back.\nFor this reason, as well as being best practice, it is always advisable to create migrations in a way that means the application can keep running as intended with either the current or new database schema. In practice, this means:\nNew columns can be added when required To delete a column, a version of the application that no longer expects it should be successfully deployed, followed by a second deployment to delete the column To rename a column, a column with the new name should be created along with a version of the application that uses it instead of the old name; once this is successfully deployed, a second deployment can delete the old column By following the steps above, you can be sure that even if your deployment fails, your application will keep running.\n","permalink":"https://engineering.resolvergroup.com/2020/09/running-database-migrations-on-deployment-for-fargate-containers/","summary":"\u003cp\u003eOne of the tenets of modern framework-based web development is the management of your database state in code. Rather like infrastructure-as-code, it means there is an immutable history of the state of the application database at any point in time, which can be re-applied or rolled back as necessary. In addition, the desired state of the database and the application are tied together at any point in time to reduce the chances of divergence.\u003c/p\u003e","title":"Running database migrations on deployment for Fargate containers"},{"content":"On 17th June 2020 I attended my first AWS summit. New to the industry, it was one of the first tech conferences I’d been to (other than the Women in Development conference earlier this year, back in that beautiful time when people were allowed to meet in groups\u0026hellip;) and was certainly the first virtual online conference I’ve been to. I didn’t quite know what to expect, but it ended up being an intense, engaging half-day where I learnt a LOT.\nThe agenda for the day was jam-packed and very diverse - 5 sessions over the course of the morning, with 11 different speakers to choose between for each one. The sessions were helpfully labelled with things like “I’m a data scientist”, “I work with emerging tech” or even “I’m new to the cloud” to help guide you to the right sessions for you, but really there were no limits and we were encouraged to branch out and try new things.\nThe introductory keynote talk given by Dr Wernel Vogels, CTO of Amazon.com set the excited tone for the rest of the day. Dressed in a black BATTLEBOTS t-shirt, he was full of child-like passion and enthusiasm, and his talk was very relevant to these strange times we’re living through, with interesting references to increased media consumption during lockdown, and projects that AWS is involved in to help keep isolated people and communities connected.\nBefore becoming a software developer this year, I worked in the charity industry for 6 years. Since my career change I have learnt that the world is not, as I had naïvely thought, split into worthwhile causes and evil corporations. The summit was another reminder that there are so many opportunities to use tech for good in the world. The keynote talk introduced amazing organisations like Wefarm and Nextdoor that are using AWS to make their work possible. There was also a fascinating (though all-too short) session from Ana Visneski, Principal Technical Program Manager of the AWS Disaster Response, where I was delighted to learn not only that AWS has a Disaster Response team, but that they have been using technology to respond to disasters like Hurricane Dorian in 2019, the Mt. Kilauea volcano eruption in Hawaii in 2018, and of course, most recently and relevantly, the COVID pandemic.\nI did attend a couple of “Introduction to…” sessions, but unfortunately in my experience, any “introductory” sessions less than an hour never seem to be appropriate for a beginner audience. Speakers can, in the same breath, go from explaining what the service even does, to meticulous detail of price plans in terms of GHz, Tbs and RAM. (That said, I did once attend an excellent 2-hour introduction session to S3, so it is possible!)\nThe highlight of my day (appealing to my geeky linguist background) was a session called “Build and deploy an Amazon Lex chatbot and convert it to an Alexa Skill”, expertly delivered by Sohan Maheshwar, Developer Advocate at AWS.\nAs Sohan said, when you think of computing devices, you might think of PCs, laptops, mobile phones, even thermostat displays or TVs with remotes. With the exception of the introduction of touch screen devices in the 2000s, user interfaces haven’t changed much since keyboards from the 70s and mice from the 80s. Recent advances in machine learning and natural language processing have for the first time made “conversational interfaces” possible, such as Amazon Alexa or Google Home. Sohan pointed out that this is a game-changer in the industry for 2 main reasons: firstly that it lowers the barrier into technology, and secondly - and the most powerful statement of the day for me - because “so far, we as humans have been forced to think like computers… but with conversational interfaces, computers are being forced to think like humans.”\nAmazon Lex is the AWS service for building conversational interfaces using both voice and text. It is a complete service, so you don’t need any machine learning experience, just a little bit of code. Lex can deal with recognising speech, converting speech to text, managing dialogue, deploying to multiple platforms and even provides analytics to help improve your chatbot.\nIf a user interacts with a Lex chatbot via speech, Lex uses ASR (Automatic Speech Recognition, the same speech API as Alexa) to convert speech to text, with a lot of language-specific context for accuracy. Once the speech is converted to text, or if a user interacts with the chatbot via text, it uses NLU (Natural Language Understanding, the same language API as Alexa) to convert natural, human conversation into a structure that computers can understand. The Lex chatbot sends this structure to the back-end of your application, where it can process it and return a response, which the Lex chatbot then presents to the user.\nThe structure that computers use to understand human conversation is built up of ‘utterances’, ‘intents’ and ‘slots’. In order to demonstrate what these are, Sohan gave this simple example of a chatbot program to order flowers.\nThe intent is the appropriate action in response to the user’s input. I like to think of it as the function you want the chatbot to call. For example, in the interaction above, the intent might be called OrderFlowers.\nThe utterances are the spoken or typed phrases from the user that trigger this intent. In the interaction above, “I would like to buy some flowers” is the utterance. Every utterance needs to be mapped to an intent, but there can also be MANY different utterances for the same intent, such as “I would like to buy some flowers”, “I want to buy some flowers”, “I’d like to order some flowers”, “I want to purchase some flowers”, etc. When building your chatbot you need to try to imagine all the utterances your users might come up with, and map all of them to your intent.\nThe slots are all the pieces of information or data that the chatbot needs in order to complete the action. You can think of them as the parameters to the intent function, or simply all the information needed to complete the order or request. In the example above, the slots are the type of flower, the date of flower collection and the time of flower collection. The slot would be FlowerType and the slot value would be Tulips. These values will vary from user to user.\nIt is very unlikely that a user will give the bot all the slots it needs to complete the action in one go. Humans tend to prefer back and forth conversation like above, rather than “I would like to order some tulips to be collected at 9am tomorrow.” To obtain all the slots of information they need, Lex bots use prompts, which can be either spoken or typed. Each slot is mapped to a prompt so that if the user doesn’t provide all the information, the bot can ask each prompt until it has enough information to fulfill the intent. For example, in the conversation above, the FlowerType slot is mapped to the prompt “What type of flowers would you like to order?”.\nAlexa ‘Skills’ are the equivalent of apps in the Apple or Play Store. There are over 100,000 Skills, and anyone can build a voice-based bot for free and upload it to the Alexa Skill store. Sohan logged into AWS Lex to demonstrate that it is very easy to export the front-end of a Lex bot (that is, its intent, prompts and utterances) as an Alexa Skill to then upload to the Skill store.\nFinally, Sohan mentioned some interesting key features to bear in mind when designing apps for text or for voice.\nApps designed for text are designed to be written and read. Therefore you might want to personalise your app’s messages with images and emojis. You can afford to give the user more information, as they have time to process and re-read if necessary. You can also present multiple options, for example: “Would you like fries or salad?”\nWhen designing apps for voice, however, they are designed to be heard and spoken to. Therefore you could personalise with speech and sound effects. You should keep the information brief, as users cannot concentrate to the same extent as if they’re reading. Amazon even has a “one breath” rule: if you can’t say your content in a single breath, then it’s probably too long! You should also present definitive choices, as if you ask a user verbally “Would you like fries or salad?”, they might simply reply “Yes”! It’s better to phrase it as “Which would you like? Fries or salad?”\nIf you’re interested in finding out more about building your own Lex bot and/or deploying it as an Alexa Skill, there is bountiful documentation available on AWS here: https://docs.aws.amazon.com/lex/latest/dg/what-is.html\nI know I’ve certainly started imagining what ‘utterances’ our customers might use if they were going to submit a Resolver complaint through Alexa, which ‘slots’ of information we’d need in order to submit their complaint for them, and what ‘prompts’ we might choose to gather those slots… Watch this space, a Resolver chatbot might be available on the Alexa Skill store before you know it!\n","permalink":"https://engineering.resolvergroup.com/2020/08/finally-forcing-computers-to-think-like-humans/","summary":"\u003cp\u003eOn 17th June 2020 I attended my first AWS summit. New to the industry, it was one of the first tech conferences I’d been to (other than the Women in Development conference earlier this year, back in that beautiful time when people were allowed to meet in groups\u0026hellip;) and was certainly the first virtual online conference I’ve been to. I didn’t quite know what to expect, but it ended up being an intense, engaging half-day where I learnt a LOT.\u003c/p\u003e","title":"Finally forcing computers to think like humans"},{"content":"Ask any developer the quickest and hackiest way of locking unauthorised users out of your website and they will probably mention HTTP Basic Auth. As an extremely simple method of authentication that is built-in to the HTTP protocol standard and supported by every web browser, it\u0026rsquo;s the universal, go-to method of setting up a username and password authentication prompt with minimal work.\nSimplicity and minimalism of course bring with them compromise, and most things you may associate with authentication systems cannot be achieved easily or at all with HTTP Basic Auth, so you would probably not consider it your primary authentication method for anything but the most basic of needs. But for our needs, it\u0026rsquo;s the perfect method of stopping casual snooping and accidental usage of a non-live system.\nWe\u0026rsquo;re building a new SaaS product, and we have both a staging system (which is non-live by definition and where we do a lot of our testing with fake data) and a production system (which will be live once the system is ready to launch, but is currently dormant). The production system will eventually be publically accessible but the staging system is only for internal development. Securing both of these systems from outside access during development is a great use case for HTTP Basic Auth.\nHow this works with Fargate There are usually two ways to implement HTTP Basic Auth - either on the web server or in the application itself. Implementing in the application does mean you don\u0026rsquo;t need to fiddle with infrastructure, but it adds more overhead to the application and can make things slower for large numbers of requests. The preference is to add the configuration at the web server level.\nHowever, with Fargate, we have no access to the underlying web server or any of the related infrastructure, and we don\u0026rsquo;t want to start adding cruft into our application.\nLuckily for us, Lambda functions are the perfect, lightweight option for running small functions a large number of times. Lambda@Edge, a specialist type of Lambda, replicates your function to all CloudFront edge locations around the world, allowing it to sit in front of requests to the CDN and run blazing fast.\nWe can use a Lambda@Edge function in conjunction with our CloudFront distribution to control access to our Fargate-backed application by using HTTP Basic Auth.\nCreating the Lambda@Edge function Let\u0026rsquo;s start by creating the function that will run with each request. This function will read and set the appropriate HTTP headers to control access using HTTP Basic Auth. We\u0026rsquo;re using JavaScript here with NodeJS:\n\u0026#39;use strict\u0026#39;; exports.handler = function(event, context, callback) { // Get request and request headers const request = event.Records[0].cf.request; const headers = request.headers; // Configure authentication credentials const authUser = \u0026#39;***your-username-here***\u0026#39;; const authPass = \u0026#39;***your-password-here***\u0026#39;; // Construct the HTTP Basic Auth string const authString = \u0026#39;Basic \u0026#39; + new Buffer(authUser + \u0026#39;:\u0026#39; + authPass).toString(\u0026#39;base64\u0026#39;); // Require HTTP Basic Auth if (typeof headers.authorization == \u0026#39;undefined\u0026#39; || headers.authorization[0].value != authString) { const body = \u0026#39;Unauthorized\u0026#39;; const response = { status: \u0026#39;401\u0026#39;, statusDescription: \u0026#39;Unauthorized\u0026#39;, body: body, headers: { \u0026#39;www-authenticate\u0026#39;: [{ key: \u0026#39;WWW-Authenticate\u0026#39;, value: \u0026#39;Basic\u0026#39; }] }, }; callback(null, response); } // Continue request processing if authentication passed callback(null, request); }; The function starts by getting the HTTP headers from the CloudFront request. It then constructs a string using the hardcoded username and password that we want to authenticate against. Finally, it checks to see whether a username and password has been provided, and if so, whether it matches what we expect.\nIf nothing is provided, or the credentials are incorrect, the standard WWW-Authenticate header is served to prompt the browser to ask the user for credentials. Otherwise, the request continues uninterrupted.\nDefining the Lambda@Edge function Now we have a function, we need to define it and upload the function to AWS.\nresource \u0026#34;aws_lambda_function\u0026#34; \u0026#34;http_basic_auth_lambda\u0026#34; { provider = aws.us-east-1 filename = \u0026#34;packages/http_basic_auth.zip\u0026#34; function_name = \u0026#34;http_basic_auth-lambda\u0026#34; handler = \u0026#34;http_basic_auth.handler\u0026#34; role = aws_iam_role.lambda_role.arn description = \u0026#34;Provide HTTP Basic Auth\u0026#34; runtime = \u0026#34;nodejs12.x\u0026#34; source_code_hash = filebase64sha256(\u0026#34;packages/http_basic_auth.zip\u0026#34;) publish = true } The function itself is contained in a file called http_basic_auth.js. This file name is important since the handler name (http_basic_auth.handler) is based on the filename and the name of the exported function inside it.\nIn order to upload the function to AWS, we need to compress it inside a zip file. On upload, the zip file is automatically uncompressed. This zip file is referred to in the definition.\nOne last thing to note is that we\u0026rsquo;re creating this function in the us-east-1 AWS region. This is important since all Lambda@Edge functions to be used with CloudFront must be in this region. The knock-on impact of this is that CloudWatch logs for this function will also reside in that region rather than whatever other region you may be using for the rest of your infrastructure.\nLinking the Lambda@Edge function to the CloudFront distribution Now that we have a Lambda@Edge function, we need to tell our CloudFront distribution to use it for each request. In a previous blog post, I showed a sample CloudFront distribution definition for assets. We\u0026rsquo;ll use something similar here and add in a link to the Lambda@Edge function:\nresource \u0026#34;aws_cloudfront_distribution\u0026#34; \u0026#34;cloudfront_cdn\u0026#34; { # ... aliases = [\u0026#34;www.example.com\u0026#34;] default_cache_behavior { allowed_methods = [\u0026#34;GET\u0026#34;, \u0026#34;HEAD\u0026#34;, \u0026#34;POST\u0026#34;, \u0026#34;PUT\u0026#34;, \u0026#34;DELETE\u0026#34;, \u0026#34;OPTIONS\u0026#34;, \u0026#34;PATCH\u0026#34;] cached_methods = [\u0026#34;GET\u0026#34;, \u0026#34;HEAD\u0026#34;] target_origin_id = \u0026#34;www-origin\u0026#34; viewer_protocol_policy = \u0026#34;redirect-to-https\u0026#34; forwarded_values { query_string = true headers = [\u0026#34;Host\u0026#34;, \u0026#34;Origin\u0026#34;] cookies { forward = \u0026#34;all\u0026#34; } } lambda_function_association { event_type = \u0026#34;viewer-request\u0026#34; lambda_arn = aws_lambda_function.http_basic_auth_lambda.qualified_arn } } origin { domain_name = [\u0026#34;alb.example.com\u0026#34;] origin_id = \u0026#34;www-origin\u0026#34; custom_origin_config { http_port = 80 https_port = 443 origin_protocol_policy = \u0026#34;https-only\u0026#34; origin_ssl_protocols = [\u0026#34;TLSv1.1\u0026#34;] } } # ... } The interesting part of the definition above is the lambda_function_association block. This block associates a Lambda@Edge function with the distribution. This function is run for every request and receives a copy of the request headers.\nThat\u0026rsquo;s it! Once that\u0026rsquo;s all built, test it out by visiting the URL you\u0026rsquo;re protecting. You should see a prompt similar to this:\nTest it out by typing a random username and password, and you should be prompted again. Now, try the username and password you previously hardcoded into the Lambda@Edge function, and you\u0026rsquo;ll see your site.\n","permalink":"https://engineering.resolvergroup.com/2020/08/implementing-http-basic-auth-for-fargate-using-lambdaedge-and-cloudfront/","summary":"\u003cp\u003eAsk any developer the quickest and hackiest way of locking unauthorised users out of your website and they will probably mention HTTP Basic Auth. As an extremely simple method of authentication that is built-in to the HTTP protocol standard and supported by every web browser, it\u0026rsquo;s the universal, go-to method of setting up a username and password authentication prompt with minimal work.\u003c/p\u003e\n\u003cp\u003eSimplicity and minimalism of course bring with them compromise, and most things you may associate with authentication systems cannot be achieved easily or at all with HTTP Basic Auth, so you would probably not consider it your primary authentication method for anything but the most basic of needs. But for our needs, it\u0026rsquo;s the perfect method of stopping casual snooping and accidental usage of a non-live system.\u003c/p\u003e","title":"Implementing HTTP Basic Auth for Fargate using Lambda@Edge and CloudFront"},{"content":"In my previous blog post, I gave a brief introduction to containerisation, and the differences between Docker and Docker Compose. Here I’m going to share the Docker Compose commands that I use on a daily basis and have found the most essential.\nIn my opinion, if you only learn 5 Docker Compose commands, they should be:\ndocker-compose up Run this command to launch your project.\nThe up command builds or creates all the services in the docker-compose file and starts a container for each of them.\nIf you add the -d flag (i.e., docker-compose up -d), it runs in ‘detached’ mode, which means the containers are set up and run in the background, and you don’t see Docker’s rather lengthy logs in your terminal.\n(To undo this and tear your services down, the command is, unsurprisingly, docker-compose down).\ndocker-compose run \u0026lt;service name\u0026gt; [\u0026lt;command\u0026gt;] The run command starts a new container for the service and runs a command in that container. If you don’t specify a command, it will use the default command as defined in your docker-compose file. You can overwrite this default command by adding a different one, e.g. docker-compose run web bash.\nIf you add the --rm flag, i.e. docker-compose run --rm web bash, this creates a temporary or “ephemeral” container, which means the container will automatically delete as soon as the command is complete.\ndocker-compose exec \u0026lt;service name\u0026gt; \u0026lt;command\u0026gt; Similar to the run command, but importantly different, the exec command runs a command in an already-running container.\ndocker-compose ps This command lists all of your project’s services, and their current status: i.e., “Up”, “Restarting”, or “Exit” if they’ve been stopped.\ndocker-compose logs This command prints the logs for the services in your app. You can also specify a service by name. Adding the -f flag, i.e. docker-compose logs -f, prints the logs in ‘follow’ mode, with live updates.\nWith all docker-compose commands, you can specify a particular service. Or if you don’t, the command will apply to all services listed in your docker-compose file. For example, you can either call docker-compose logs to view the logs on all the services for your project. Or if you only want to see the logs for your service called “web”, you can call docker-compose logs web.\nDocker commands can be long and repetitive, especially once you’re adding options flags, service names and commands. In our project, we created the following bin files for the commands we used frequently:\nbin/build =\u0026gt; docker-compose build $@ bin/bundle =\u0026gt; bin/exec web bundle $@ bin/destroy =\u0026gt; bin/down --rmi ‘all’ --volumes bin/down =\u0026gt; docker-compose down --remove-orphans $@ bin/exec =\u0026gt; docker-compose exec $@ bin/logs =\u0026gt; docker-compose logs $@ bin/run =\u0026gt; docker-compose run --rm $@ bin/start =\u0026gt; docker-compose start $@ bin/status =\u0026gt; docker-compose ps bin/stop =\u0026gt; docker-compose stop $@ bin/up =\u0026gt; docker-compose up $@ Make sure to preface every file with the shebang #!/bin/sh. The shebang is a special character sequence in a script file that specifies which program should be called to run the script. Here, the shebang specifies /sh, meaning the system should use its default shell program to interpret the script in these files.\nAlso, I learnt that by adding $@, the command can take any additional argument(s) you provide, but equally will run without them if you don’t.\nYou can then run the docker-compose command you need just by calling the appropriate bin file. For example, instead of typing docker-compose ps, you can now just type bin/status. And instead of typing docker-compose logs -f web, you can now just type bin/logs -f web.\nObviously, there’s a lot more to the world of Docker Compose than these 5 commands. In my next blog post, I will talk through some other commands that I have found useful during the project. But hopefully, these 5 will help get you started!\n","permalink":"https://engineering.resolvergroup.com/2020/08/if-you-only-learn-5-docker-compose-commands-it-should-be-these/","summary":"\u003cp\u003eIn my \u003ca href=\"http://a-beginners-guide-to-docker-and-containerisation\"\u003eprevious blog post\u003c/a\u003e, I gave a brief introduction to containerisation, and the differences between Docker and Docker Compose. Here I’m going to share the Docker Compose commands that I use on a daily basis and have found the most essential.\u003c/p\u003e\n\u003cp\u003eIn my opinion, if you only learn 5 Docker Compose commands, they should be:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003ccode\u003edocker-compose up\u003c/code\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eRun this command to launch your project.\u003c/p\u003e\n\u003cp\u003eThe \u003ccode\u003eup\u003c/code\u003e command builds or creates all the services in the docker-compose file and starts a container for each of them.\u003c/p\u003e","title":"If you only learn 5 Docker Compose commands, it should be these"},{"content":"I joined Resolver in February 2020 and was put into the Accord team, working on a brand new project (they had literally typed the words ‘rails new’ the week before I joined). This meant I would be involved from the ground up, that I would see the app grow and develop from the very start, and that I wouldn’t need to get to grapple with years of legacy code.\nAs we had decided to build the app with Docker, which I haven’t used before, it also meant I was about to get to grips with it very quickly! This whale of a task initially felt quite daunting, but thanks to the gloriously comprehensive Docker documentation, Linux Academy courses about Orchestration, Containers and Docker, Rob Isenberg’s invaluable Docker for Rails Developers book, and, of course, a lot of Googling, I’m now happy to share my understanding of containers and Docker in my next few blog posts. I hope they can help make the topic a little less confusing!\nWhat is containerisation? As I’m sure all developers can verify, there is nothing more frustrating than when your beautifully crafted code works perfectly on your machine, but not on your colleague’s. Or when code that works perfectly in development or staging suddenly no longer works when it deploys to production.\nContainerisation is designed to stop that happening, and to ensure that code can run consistently and reliably in any environment. The container provides the specific environment that the code needs to run, regardless of the machine it’s running on.\nThink of your codebase as an awkwardly shaped jigsaw piece that you’ve built on your own machine and fits perfectly, but might not fit so well into someone else’s. A container wraps that awkward piece in a neat square box so that it has everything it needs and will fit everywhere.\nA container is sometimes referred to as an “isolated execution environment” to run your software in. It is an “execution environment” because the container holds everything your code needs to run - i.e., the file system, the environment variables, even scripts. And it is “isolated” because as long as all these things are inside the container, they don’t need to exist on your computer. For example, if you install Python inside a container, you can run Python scripts inside that container, even if Python isn’t installed on your machine.\nWhere do containers come from? Containers are built from images. Every container has a unique environment because of a unique combination of all the things mentioned above - the file system, commands, dependencies, scripts etc. The image is the file that provides the instructions to build this unique environment for a container. The image is like the template instructions to build containers in that design.\nI find it helpful (with my Ruby mind) to think of an image like a class, and every container built from that image as an instance of that class.\nIt might sound daunting to set up an entire working environment for your code from scratch. Thankfully, there are base images available, which are beautifully crafted and publicly available images that are pre-configured with a basic environment you might need for your project. You can tweak and enhance them with specific additions for your project, taking a lot of the scary and time-consuming preparation work out of setting up your Docker environment.\nWhat are containers used for? As if ensuring your code runs reliably and consistently across machines and environments wasn’t enough, containers are useful for various scenarios, including:\nMicroservices, or splitting an application into multiple individual applications that can be built, updated and scaled separately. For example, you could even use different languages for each microservice, and therefore across your application.\nMoving existing software into the cloud. It is relatively easy to wrap existing software in containers. Containers also use fewer resources so you can save money on cloud space.\nAutomated scaling, or automatically increasing or decreasing resources as the number of visitors to (part of) your application changes. Containers can be set up and taken down very quickly in response to change in traffic, which reduces costs.\nAdvantages of using containers As mentioned, the biggest and most obvious advantage of using containers is that it allows you to run your software across machines and systems reliably, meaning no “well, it works on my machine” conversations with your colleagues or nail-biting deployments to make sure nothing has broken.\nContainers are sometimes compared to virtual machines. However, unlike virtual machines, containers don’t need a copy of the whole operating system - just your application and a definition of all the required binaries and libraries. They therefore use fewer resources and less disk space than virtual machines, meaning they start up faster and you can fit more on your computer. The only downside is that containers are slightly less flexible than virtual machines - for example, you can’t run a Windows container on a Linux machine, although you can run a Linux container on a Windows machine (but who uses Windows anyway?).\nOther benefits of using containers include being able to share libraries across containers, run different programs in isolation, and faster and simpler automation.\nWhat is Docker and Docker Compose? So, I’ve managed to write nearly a whole Docker blog post without mentioning Docker. Docker is arguably the industry leader in container technology.\nDocker builds images by reading the instructions in your Dockerfile (a text document that contains all the command line commands needed to assemble an image). You can build images from a Dockerfile with the command docker build.\nYou can store all your Docker images in Docker Hub. Docker Hub hosts repositories, which hold images, in the same way that GitHub hosts repositories of code. A Docker Hub repository includes useful information about the image, such as a short and full description, a pull link, and documentation. You can copy Docker images from Docker Hub onto your machine with the command docker pull.\nWhile a Dockerfile builds single images and containers, a Docker Compose file is a higher level definition used by the Docker Compose tool, which describes how to build multiple containers that fit together to form an entire application.\nI will be writing more blog posts in future about what I’ve learnt so far about Docker, exploring topics like the Dockerfile and Docker Compose file, useful Docker commands, and specifically, setting up a Rails application in Docker.\n","permalink":"https://engineering.resolvergroup.com/2020/07/a-beginners-guide-to-docker-and-containerisation/","summary":"\u003cp\u003eI joined Resolver in February 2020 and was put into the Accord team, working on a brand new project (they had literally typed the words ‘\u003ccode\u003erails new\u003c/code\u003e’ the week before I joined). This meant I would be involved from the ground up, that I would see the app grow and develop from the very start, and that I wouldn’t need to get to grapple with years of legacy code.\u003c/p\u003e\n\u003cp\u003eAs we had decided to build the app with Docker, which I haven’t used before, it also meant I was about to get to grips with it very quickly! This whale of a task initially felt quite daunting, but thanks to the gloriously comprehensive \u003ca href=\"https://docs.docker.com/reference/\"\u003eDocker documentation\u003c/a\u003e, Linux Academy courses about Orchestration, Containers and Docker, Rob Isenberg’s invaluable \u003cem\u003eDocker for Rails Developers\u003c/em\u003e book, and, of course, a lot of Googling, I’m now happy to share my understanding of containers and Docker in my next few blog posts. I hope they can help make the topic a little less confusing!\u003c/p\u003e","title":"A beginner’s guide to Docker and containerisation"},{"content":"\nOn 1st July, a team of Resolver developers made up of both Accord and Consumer members joined forces to compete in the AWS GameDay. The premise of the day is that it is our induction at a brand new start-up ‘Unicorn Rentals’, a fictitious company who dominate the Legendary Animal Rental Market (LARM) by providing temporary use of a unicorn to ever-demanding children.\nThe developers at Unicorn Rentals have recently moved on to other ventures, leaving a lot of legacy services behind them. It was our job to try to move away from these to ensure faster technology and therefore more unicorns! The strategy throughout the day to earn as many points as possible (and therefore beat the other teams) was to keep our router running, to launch microservices so that other teams can use them and route to as many other teams microservices (especially fast ones!) as possible. Points were lost at any moment for sending messages to our own microservices and for our router not working.\nUnfortunately, it was not as simple as getting the above set up and then watching the points roll in! Seemingly every time we appeared to have a steady stream of points being awarded to us, our focus would turn to enabling alarms and bettering infrastructure, yet before we could finish doing so, the points would stop and we needed to turn our attention to finding out what was wrong and how to fix it.\nAt times it was challenging to remember how everything on AWS works and to then go on to identify what the issue was. Throughout the day, we faced trouble relating to sudden spikes in traffic that needed scaling, containers crashing and bad app deployments! We all gained a sense of how important auto scaling instances are so that services can be maintained even if containers crash. Also spending time to debug through useful logging techniques combined with having good health check notification tools in place were all takeaways from the day.\nAt the end of the day, it came to light that the organisers of the event had been fiddling with teams settings in order to simulate problem solving in the real world. Luckily, problems do not occur this often or fast at Resolver, yet it was an educational day for all as we were able to find solutions inside a gamified and risk-free environment. We mainly worked together as a team to find solutions, splitting off into pairs or individually where needed to focus on fixing isolated problems. This approach was useful as it lead to a shared understanding yet unfortunately it did not lead us to victory. The teams that came in first and second place shared after the event that they worked largely separated throughout the day - dividing jobs between team members.\nYet, in some respects, victory is ours as it was a great opportunity to work with colleagues who are not usually in our team and to build on various levels of AWS knowledge. Also, we did win a prize for best comeback after a somewhat slow but steady start, so we were all very pleased with that. Hopefully, the unicorn’s put a smile on some virtual children\u0026rsquo;s faces, too!\n","permalink":"https://engineering.resolvergroup.com/2020/07/aws-gameday-2020/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/uploads/2020/07/image1.png\"\u003e\u003c/p\u003e\n\u003cp\u003eOn 1st July, a team of Resolver developers made up of both Accord and Consumer members joined forces to compete in the \u003ca href=\"https://aws.amazon.com/gameday/\"\u003eAWS GameDay.\u003c/a\u003e The premise of the day is that it is our induction at a brand new start-up ‘Unicorn Rentals’, a fictitious company who dominate the Legendary Animal Rental Market (LARM) by providing temporary use of a unicorn to ever-demanding children.\u003c/p\u003e\n\u003cp\u003eThe developers at Unicorn Rentals have recently moved on to other ventures, leaving a lot of legacy services behind them. It was our job to try to move away from these to ensure faster technology and therefore more unicorns! The strategy throughout the day to earn as many points as possible (and therefore beat the other teams) was to keep our router running, to launch microservices so that other teams can use them and route to as many other teams microservices (especially fast ones!) as possible. Points were lost at any moment for sending messages to our own microservices and for our router not working.\u003c/p\u003e","title":"AWS GameDay 2020"},{"content":"There was a time when asset storage was one of the problems facing web app developers - maybe you stored assets on the web server (easy if you only have one, more difficult if you need to sync them up between multiple servers), or maybe you had a shared disk mounted using NFS. In any case, the easier part was granting access to these assets to the right users. Given that assets are normally stored outside the web server\u0026rsquo;s root directory, it\u0026rsquo;s easy to write a script that checks user access then serves the asset.\nThe downside of serving your own assets is firstly that you need somewhere to store them (and hopefully back them up) and secondly that it uses up your precious bandwidth (an issue if you pay for a limited amount of bandwidth per month, for example). However, there is a better way - using a cloud storage solution like S3 from AWS.\nS3 allows you to store and serve assets quickly, easily and most of all, very cheaply, especially if you basically outsource the serving directly to S3 and bypass your web server entirely. The downside to this is that you lose control of any app-based access controls.\nWhy S3 pre-signed URLs don\u0026rsquo;t work here The simplest answer here is to make use of S3 pre-signed URLs. These are time-limited URLs containing a signing key that is generated from a set of metadata and AWS credentials. If you\u0026rsquo;re using the default S3 URLs, then this is a simple and viable strategy to grant time-limited access to assets in an S3 bucket that are normally only accessible to the owner of the bucket.\nIn this case however, the objective is to use CloudFront to sit in front of S3 both for CDN and caching purposes, and also because it allows use of a custom domain name to access the assets, as well as allowing much more control of things like HTTP headers.\nThe problem stems from the fact that CloudFront itself needs to be able to access S3. Once this access is granted, then there is no way for S3 to determine whether the end user accessing the asset via CloudFront is authorised or not - from its point-of-view, all requests originate with CloudFront.\nEnter CloudFront signed URLs Happily, CloudFront has its own implementation of signed URLs. These are signed at the CloudFront level and can be used to grant time-limited access to any CloudFront-fronted content, not just assets in an S3 bucket.\nIn our case, we\u0026rsquo;ll use CloudFront signed URLs in our Ruby code to implement time-limited access to sensitive data to only those users who are entitled to access it.\nThe S3 bucket and pre-requisites Before we go ahead and create an S3 bucket, we need two pre-requisites. First up, it\u0026rsquo;s a CloudFront origin access identity (OAI).\nresource \u0026#34;aws_cloudfront_origin_access_identity\u0026#34; \u0026#34;cloudfront_assets_proxy_origin_access_identity\u0026#34; { comment = \u0026#34;CloudFront origin access identity for assets\u0026#34; } The CloudFront OAI is a construct that allows us to grant access to CloudFront to serve files from an S3 bucket. We use it for this next pre-requisite, which is to create a bucket access policy.\ndata \u0026#34;template_file\u0026#34; \u0026#34;assets_bucket_policy_template\u0026#34; { template = file(\u0026#34;assets_bucket_policy.json\u0026#34;) vars = { assets_s3_bucket = \u0026#34;arn:aws:s3:::assets\u0026#34; cloudfront_oai_iam_arn = aws_cloudfront_origin_access_identity.cloudfront_assets_proxy_origin_access_identity.iam_arn } } The content of the policy is:\n{ \u0026#34;Version\u0026#34;: \u0026#34;2012-10-17\u0026#34;, \u0026#34;Id\u0026#34;: \u0026#34;AssetsBucketPolicy\u0026#34;, \u0026#34;Statement\u0026#34;: [ { \u0026#34;Effect\u0026#34;: \u0026#34;Allow\u0026#34;, \u0026#34;Action\u0026#34;: [ \u0026#34;s3:GetObject\u0026#34; ], \u0026#34;Principal\u0026#34;: { \u0026#34;AWS\u0026#34;: \u0026#34;${cloudfront_oai_iam_arn}\u0026#34; }, \u0026#34;Resource\u0026#34;: [ \u0026#34;${assets_s3_bucket}/*\u0026#34; ] } ] } This policy grants permission to the CloudFront OAI to get objects from the S3 bucket we\u0026rsquo;re about to create. Later when we create the CloudFront distribution, we\u0026rsquo;ll give it this OAI to use akin to an authentication token or IAM role.\nNow we\u0026rsquo;re able to create an S3 bucket to hold the assets.\nresource \u0026#34;aws_s3_bucket\u0026#34; \u0026#34;assets\u0026#34; { bucket = \u0026#34;assets\u0026#34; policy = data.template_file.assets_bucket_policy_template.rendered versioning { enabled = true } } resource \u0026#34;aws_s3_bucket_public_access_block\u0026#34; \u0026#34;assets_public_access_block\u0026#34; { bucket = aws_s3_bucket.assets.id block_public_acls = true block_public_policy = true } We apply the bucket access policy to our new bucket and enable versioning (optional). We then also block any public permissions from accidentally being applied to the bucket in future as a guard.\nThe CloudFront distribution Next, we need a CloudFront distribution so that we can access the S3 bucket (some details such as TLS certificate setup have been removed for clarity).\nresource \u0026#34;aws_cloudfront_distribution\u0026#34; \u0026#34;cloudfront_assets_proxy\u0026#34; { # ... aliases = [\u0026#34;assets.example.com\u0026#34;] default_cache_behavior { allowed_methods = [\u0026#34;GET\u0026#34;, \u0026#34;HEAD\u0026#34;, \u0026#34;OPTIONS\u0026#34;] cached_methods = [\u0026#34;GET\u0026#34;, \u0026#34;HEAD\u0026#34;] target_origin_id = \u0026#34;assets-bucket-origin\u0026#34; trusted_signers = [\u0026#34;self\u0026#34;] viewer_protocol_policy = \u0026#34;redirect-to-https\u0026#34; forwarded_values { query_string = true cookies { forward = \u0026#34;none\u0026#34; } } } origin { domain_name = aws_s3_bucket.assets.bucket_regional_domain_name origin_id = \u0026#34;assets-bucket-origin\u0026#34; s3_origin_config { origin_access_identity = aws_cloudfront_origin_access_identity.cloudfront_assets_proxy_origin_access_identity.cloudfront_access_identity_path } } # ... } This time, we get to use the dedicated S3 origin type for our distribution.\nThe important things to note are:\nWe only allow GET and similar HTTP methods We list self as a trusted signer (more on this below) We set up redirection to HTTPS (the TLS setup has been omitted here) We forward all query string values to S3 (more on this below too) We configure the S3 origin with the OAI we set up earlier to grant access The Route 53 DNS record As always, we need a DNS record for assets.example.com that points it to the CloudFront distribution.\nresource \u0026#34;aws_route53_record\u0026#34; \u0026#34;cloudfront_assets_proxy_service_record\u0026#34; { zone_id = \u0026#34;ABC\u0026#34; name = \u0026#34;assets.example.com\u0026#34; type = \u0026#34;CNAME\u0026#34; ttl = 300 records = [aws_cloudfront_distribution.cloudfront_assets_proxy.domain_name] } Using this setup in our app Now we have all the infrastructure set up, we can start signing URLs in our app.\nIn this case, we\u0026rsquo;re using Rails\u0026rsquo; built-in ActiveStorage component with the S3 configuration to handle the uploading and management of files, and the cloudfront-signer gem to provide methods for signing our URLs. We enabled self to generate signed URLs above, which means anything running under the context of the same AWS account.\nWe use the root account for AWS to generate a CloudFront public/private key pair which we set up using an environment variable called CLOUDFRONT_PRIVATE_KEY.\nWe then start by configuring the gem with a new initialiser in config/initializers:\nAws::CF::Signer.configure do |config| config.key = ENV[\u0026#34;CLOUDFRONT_PRIVATE_KEY\u0026#34;] config.key_pair_id = \u0026#34;ID HERE\u0026#34; config.default_expires = 3600 end We provide the private key from the key pair and the ID which can be obtained when generating the key pair.\nWe then define a method that takes a given ActiveStorage file and generates a signed URL for it:\ndef signed_attachment_url(blob) filename = blob.filename.sanitized key = blob.key content_type = blob.content_type # Every component of the URL must be URL encoded otherwise signed URLs will not work Aws::CF::Signer.sign_url(\u0026#34;\u0026lt;https://assets.example.com/#{key}?response-content-disposition=inline%3B%20filename%3D%22#{filename}%22\u0026amp;response-content-type=#{content_type}\u0026gt;\u0026#34;) end The method takes an ActiveStorage blob (which represents a file in S3) and constructs a URL for it using the domain we set up as a CloudFront distribution, the key (which is the name the file is stored under in S3) and a couple of query string entries which define the original file name and content type (this is why we enabled query string forwarding above).\nThe given content type is returned by S3 as an HTTP header with the file to allow the browser to determine how to display the file. The file name is also returned as an HTTP header so if the user decides to download or save the file, it\u0026rsquo;ll get a usable file name rather than a random key.\nNote as per the comment that you must ensure the entire URL is URL encoded wherever needed as otherwise, the signed URL will not match and no content will be returned.\nWe can now call this method throughout our application to get a time-limited URL that we can present to users.\n","permalink":"https://engineering.resolvergroup.com/2020/07/granting-time-limited-access-to-assets-in-s3-using-cloudfront/","summary":"\u003cp\u003eThere was a time when asset storage was one of the problems facing web app developers - maybe you stored assets on the web server (easy if you only have one, more difficult if you need to sync them up between multiple servers), or maybe you had a shared disk mounted using NFS. In any case, the easier part was granting access to these assets to the right users. Given that assets are normally stored outside the web server\u0026rsquo;s root directory, it\u0026rsquo;s easy to write a script that checks user access then serves the asset.\u003c/p\u003e","title":"Granting time-limited access to assets in S3 using CloudFront"},{"content":"A day at Makers was a lot like going to a theme park with your pals:\nThe eager excitement of waking up and getting ready. Meeting your friends for breakfast, fueling up on coffee, and making all the best-laid plans for what is about to come. Enjoying all the sights and sounds of wonderment and magic. Going for lunch and exploring the area for delicious food. Tackling the big rollercoasters, and experiencing all the highs and lows. Leaving exhausted, and desperate to come back.\nEach day at Makers really was an event - you spend your days fully immersed in code and all the different practices around it. You and 20 or so other like-minded individuals are hurled together into a monthly cohort, and from day 2, (day one generally being a meet and greet) become fully engaged in a whirlwind of workshops, reviews, projects, and tasks. It really is enough to take your breath away. Thankfully, your cohort quickly turns into a family, and from this a support network of people encouraging and helping you, not competing against you, help you strive to be the best version of yourself. The results really are incredible. I can honestly say that some of the people who I met on the course are amongst my best friends today and that I grew so much as an individual.\nA typical day 08:00 - 09:00 Breakfast (optional)\nThe most important time of the day! This is the time to get caffeinated, relax, and catch up with friends before the day starts.\n09:00 - 09:30 Peer Group Review / Catch-up\nAt the beginning of the course, you are arranged into smaller groups of your cohort, roughly 4-6 people - your peer groups. These are people who keep you honest and on track, making sure you stick to your goals and being on hand to support you with your aims and achievements. This time is for goal setting for the day, reviewing whether you achieved your goals from the previous day, sharing knowledge, and working through any blockers.\n09.30 - 12.30 Self Learning Time / Classes\nThis time is set aside for personal objectives in terms of learning, exploring particular principles, or working through specific exercises. Makers provide a wide variety of resources and this is the time to decide what your time is best spent working on in order to be successful. Typically each week of the course will revolve around a specific theme of languages or practices with week-by-week repositories of resources supplied. As a general rule, it is important to keep looking forward rather than back. There is too much information to do it all, so get used to not finishing all your work before moving on to the next thing!\nThis time is also when most of the lectures and workshops are scheduled; some of these are optional, but all of them are valuable.\nFinally, coaches (who aren’t running lectures) are available during this time to help with technical issues and questions.\n12:30 - 13:30 Lunch\nTime to explore! Thankfully Makers is perfectly located to make the most out of excellent food spots. This is a great way to socialize and let all the knowledge of the morning sink in.\n13:30 - 14:00 Meditation\nMakers offers a daily meditation session that is an invaluable counter to the chaos of the course. It helps to keep everything in perspective and keeps the mind fresh and responsive.\n14:00 - 17:00 (optionally longer) Weekly Pairing Challenge\nThis is the big one - the time everyone is waiting for! Each week, there is a ‘weekly challenge’ putting into practice what you have learned and coming up against new obstacles faced for the first time in the form of a huge week-long project. Each day of a weekly challenge, you are paired with someone new from the cohort. This means that you get to work with absolutely everyone in the cohort before it resets and goes around again. This is an excellent chance to meet everyone and find out how everyone works as well as practicing your interpersonal skills. Some people are passive and some people are dominant. The valuable skill here, as well as the coding, is being in a real-life environment where you are at some point going to have to work with a wide variety of people with different styles and quirks and pair-program with them. It also allows you to practice both sides of the coin in terms of being ahead of or behind your partner from the previous day, explaining and reinforcing your knowledge to someone still trying to work it out, or being able to explain what you would do to someone who knows the answer.\nTypically there is also weekend work, which is a shorter version of the weekly challenge but intended to be completed solo and with no external help. This is then evaluated by the cohort on Monday morning.\nThe main thing I wish I knew before I started The self-inflicted lack of personal/free time! The core hours for the course were roughly 09:00 - 17:00 and that was also including weekends, (of which you only earned your weekends back after week 8 when there are no more weekend challenges) however I found that the biggest reason for my lack of free time was because I wanted to code all the time, learning as much as I could and do the best I could at everything. It was definitely something that I found addictive (in a good way) because I found it so rewarding. I would get in early and leave late because I was genuinely enjoying myself. That being said, a balance must be found, and keeping up with social responsibilities with friends and family, making sure you get the right amount of exercise, and mental downtime is a must. Altogether this meant that all my days were jam-packed for an extended time.\nMy biggest realisation once I had finished Just how much I had actually learned! At times Makers felt like you were barely keeping your head above the waves. New concepts and tasks are thrown at you, workshops leave you more confused than you were before you went in, you can’t fix that annoying bug, and all your tests are failing! However, the phrases that kept getting told to us were ‘just keep swimming/coding’ and ‘it’s not hard, it’s new’. At the time, these feel meaningless and are frustrating to hear, however, post-course is the only time you have for a full reflection of everything. When you eventually look down from the mountain you have been climbing it’s a long way down! Even things you’re still not sure you understand are no more than a few smart searches away and with all the information you have acquired, the pieces of the puzzle start falling into place. You’ll be amazed at how far you’ve come from those first tentative lines of code you wrote in the first week’s challenge.\nThe most challenging thing about the course The most difficult part of the course for me was learning to learn again. I didn’t go to university and have only ever known a traditional schooling system which was around 10 years ago. I was completely out of practice when it came to rigorous learning for 8+ hours a day. Many times in the early days, I found that I would ask a question only to get the response ‘have you looked into it?’ or ‘what have you done to find the answer?’ and having to go away to do my own research. It was frustrating at the time, thinking that if they just told me the answer I would have known it a whole lot quicker. What I didn’t realise is that they were setting me up to be a fully autonomous self-learning machine! This realisation came about halfway through the course where I had a lucid moment, realising that I hadn’t asked a question in a long time due to me finding the answers myself. This was incredibly empowering and built up my confidence and learning, making problem-solving faster and more efficient and also improving my quality of code, due to me finding out the answers quicker and being able to join the dots.\nWhat’s next? I would recommend Makers or any similar coding boot camp to anyone who is interested in a career in software development/engineering. It gave me the specific technical skills I never had, and it reinforced the soft skills I needed. I definitely grew throughout the whole process and I feel like a more ambitious and confident person.\nThe post-course activities at Makers consisted of job hunting and continuing to practice the skills I had learned by working on more projects, this was a tough process and could be a story all of its own, but the short of it is that I was lucky enough to secure my first job in development off the back of what was an incredible experience.\nI went through a full interview process of face-to-face interviews (or screen-to-screen video calls in the wake of Covid-19) and tech tests to eventually get a job offer from Resolver Group. Resolver is the market leader in consumer complaint resolution and I am incredibly grateful to be able to join the team starting my journey as a fully employed developer. So far, my start to developer life has been brilliant. I have been put on courses to further my knowledge specific to their tech stack and everyone has done a fantastic job of making me feel welcome. Now onto the real work of picking up tickets and making a positive impact\u0026hellip;\n","permalink":"https://engineering.resolvergroup.com/2020/06/making-a-maker-a-day-in-the-life-of-a-coding-bootcamp-student/","summary":"\u003cp\u003eA day at Makers was a lot like going to a theme park with your pals:\u003c/p\u003e\n\u003cp\u003eThe eager excitement of waking up and getting ready. Meeting your friends for breakfast, fueling up on coffee, and making all the best-laid plans for what is about to come. Enjoying all the sights and sounds of wonderment and magic. Going for lunch and exploring the area for delicious food. Tackling the big rollercoasters, and experiencing all the highs and lows. Leaving exhausted, and desperate to come back.\u003c/p\u003e","title":"Making a Maker: a day in the life of a coding bootcamp student"},{"content":"A common issue for developers and operations staff when it comes to domain names is whether to choose the apex domain (e.g. example.com) or the www subdomain (e.g. www.example.com) as their primary domain, and then how to redirect one to the other.\nChoosing to use www over the apex At Resolver, we use AWS for the majority of our infrastructure, and we generally have staging and production environments for each product or service we build. We use subdomains as namespaces to separate the environments whilst providing similar URLs for each.\nAs an example, for a hypothetical product hosted at example.com, the apex domain (example.com) is effectively the namespace for our production environment, whilst the staging subdomain (staging.example.com) is the namespace for our staging environment. Given that we will be using subdomains (such as app.example.com or app.staging.example.com for our web app), it makes sense to also have a subdomain for the main site itself, which conveniently can be www (as in www.example.com or www.staging.example.com).\nFor the staging environment, this is fine since we don\u0026rsquo;t expect anyone to be visiting staging.example.com, but it is a reasonable assumption that some people will try to visit example.com, and without a redirect in place, they\u0026rsquo;ll be confused about why they can\u0026rsquo;t access the site.\nRedirecting at the CDN level By choosing to use the existing CDN to handle the redirect from the apex domain to the www subdomain, we avoid having to deal with this either in our app (slow and extra logic to maintain) or on our web server (extra configuration and bandwidth charges, and not available if you use containerised solutions such as ECS).\nThis technique requires a CloudFront distribution and an S3 bucket. So let\u0026rsquo;s see how it\u0026rsquo;s done.\n(As a side note, in this and future technical posts we\u0026rsquo;ll generally use Terraform\u0026rsquo;s HCL syntax when we\u0026rsquo;re demonstrating infrastructure since it allow a concise textual representation).\nThe S3 bucket Firstly, we need an S3 bucket which acts as the origin for the CloudFront distribution. The bucket will be empty but we enable the website hosting feature and configure it to redirect all requests.\nresource \u0026#34;aws_s3_bucket\u0026#34; \u0026#34;apex_domain_redirect_bucket\u0026#34; { bucket = \u0026#34;example.com\u0026#34; acl = \u0026#34;public-read\u0026#34; website { redirect_all_requests_to = \u0026#34;https://www.example.com\u0026#34; } } The bucket is named after our apex domain, and allows public read (which is necessary for the redirect to work). All requests are redirected to our www subdomain.\nThe CloudFront distribution Next, we need a CloudFront distribution so that we can access the S3 bucket and its redirect from our apex domain (some details such as TLS certificate and caching setup have been removed for clarity).\nresource \u0026#34;aws_cloudfront_distribution\u0026#34; \u0026#34;apex_domain_redirect_cloudfront_cdn\u0026#34; { # ... aliases = [\u0026#34;example.com\u0026#34;] default_cache_behavior { # ... target_origin_id = \u0026#34;apex-domain-cloudfront-origin\u0026#34; # ... } origin { domain_name = aws_s3_bucket.apex_domain_redirect_bucket.website_endpoint origin_id = \u0026#34;apex-domain-cloudfront-origin\u0026#34; custom_origin_config { http_port = 80 https_port = 443 origin_protocol_policy = \u0026#34;http-only\u0026#34; origin_ssl_protocols = [\u0026#34;TLSv1.1\u0026#34;] } } # ... } Even though there is a dedicated CloudFront origin type for S3 buckets, that is only designed for when you\u0026rsquo;re serving files from that bucket. Here, we\u0026rsquo;re using the website hosting capability of S3, which means we need to use the custom origin configuration instead.\nWe set the origin to the website endpoint of our S3 bucket (which looks something like http://example.com.s3-website.eu-west-2.amazonaws.com) and we configure it as HTTP-only (S3 website endpoints don\u0026rsquo;t support HTTPS, but you can terminate TLS in CloudFront by configuring a certificate in the distribution).\nAt this point, we have our CDN (accessible at something-random.cloudfront.net), which sends all requests to S3, which replies with a redirect to www.example.com.\nThe Route 53 DNS record Finally, we need to add a DNS record for example.com that points it to the CloudFront distribution to complete the setup and allow the apex domain to redirect to the www subdomain.\nresource \u0026#34;aws_route53_record\u0026#34; \u0026#34;apex_domain_redirect_cloudfront_cdn_service_record_ipv4\u0026#34; { zone_id = \u0026#34;ABC\u0026#34; name = \u0026#34;example.com\u0026#34; type = \u0026#34;A\u0026#34; alias { name = aws_cloudfront_distribution.apex_domain_redirect_cloudfront_cdn.domain_name zone_id = aws_cloudfront_distribution.apex_domain_redirect_cloudfront_cdn.hosted_zone_id evaluate_target_health = false } } Given we\u0026rsquo;re using both Route 53 and CloudFront, we can make use of alias records. This is useful since ideally we\u0026rsquo;d use CNAMEs (to allow for changing CloudFront IP addresses without having to update our records), but they are not allowed for apex domains since they cannot co-exist with any other record type (and apex domains have SOA and NS records at a minimum). Alias records are provider-specific records that act like CNAMEs but present themselves as A records pointing to an IP address.\nThe final view Now we have all the puzzle pieces in place, the final user journey looks like this:\nRequest → Route 53 → CloudFront → S3 → Redirect → CloudFront → User\nWith a simple mix of a CDN and website redirector, we have a lightweight redirection service that we don\u0026rsquo;t have to maintain. This technique is not just limited to apex domain redirects and can also be useful for other similar fixed redirects where your app doesn\u0026rsquo;t need to be involved.\n","permalink":"https://engineering.resolvergroup.com/2020/06/how-to-redirect-an-apex-domain-to-www-using-cloudfront-and-s3/","summary":"\u003cp\u003eA common issue for developers and operations staff when it comes to domain names is whether to choose the apex domain (e.g. \u003ccode\u003eexample.com\u003c/code\u003e) or the www subdomain (e.g. \u003ccode\u003ewww.example.com\u003c/code\u003e) as their primary domain, and then how to redirect one to the other.\u003c/p\u003e\n\u003ch2 id=\"choosing-to-use-www-over-the-apex\"\u003eChoosing to use www over the apex\u003c/h2\u003e\n\u003cp\u003eAt Resolver, we use AWS for the majority of our infrastructure, and we generally have staging and production environments for each product or service we build. We use subdomains as namespaces to separate the environments whilst providing similar URLs for each.\u003c/p\u003e","title":"How to redirect an apex domain to www using CloudFront and S3"},{"content":"Remote working isn’t a new thing for developers; at Resolver we were already doing it regularly. Our meetings were remote-friendly, if not fully remote, and Slack would keep us all constantly connected. So when we went into lockdown in March, the transition to home-working was perhaps easier for our team than for others. But when it came to hiring new developers, removing any face-to-face contact posed some new challenges.\nNetworking events The hiring process would often begin by speaking at events such as LRUG, giving a summary of Resolver and what we’re striving for. That gave us an opportunity to meet any interested candidates straight away and to make those important first impressions. Under lockdown, we’ve had to approximate that by posting to mailing lists, posting to LinkedIn and using recruiters. We’ve seen some good results doing that, and the market for Ruby developers still seems to be strong. But it can also be easy for the message to get lost in the noise. Emphasising what sets Resolver apart as a great place for developers has been more important than ever.\nFace-to-face interviews After an initial conversation, we’ll ask candidates to complete a coding task in their own time. From there, we’d usually have a face to face interview in our office. This allowed us to have an in-depth discussion of the task, to pair with the candidate and to ask some more general questions about background and interests. We’ve been able to achieve most of that with Google Hangouts, which we use internally for most of our meetings. But it made us question how we could improve our pair-programming in general.\nBefore lockdown, pairing would either be done with Use Together, or by just sitting together and sharing a keyboard. Use Together is great on the whole, but I’ve found that it can be quite slow and CPU intensive when used for a long time. There’s also a cost for using it for over 30 minutes.\nMany of us also use Visual Studio Code as our main editor, and its Live Share extension is very impressive. You can sign in with GitHub or Microsoft and you’re given a link to share with anyone. I’ve found the performance to be good over long sessions, and your pair can even see your localhost from their own browser. And it’s free!\nThere are many tools which offer similar things; AWS Cloud9 is another good option. While these tools are great for internal use, they do require some initial setup to get started, so they can often lead to delays with remote interviews. A better alternative in my opinion, is tools like HackerRank’s CodePair. They’re available from the browser and they’ve been built specifically with interviews in mind.\nTeam lunches Once a candidate is hired, we would usually go to a restaurant around London Bridge, as an introduction to the rest of the team. We’ve tried to achieve something similar with Google Hangouts, and with informal introductions over a coffee. In fact, we now always have a Hhangout available through Slack, so at any point we can “go for a coffee” and have a chat. We’ve found that to be a great way to bring new developers into the team.\nAfter lockdown My feeling is that most of these practices will return to normal once the lockdown is over and we’re back in the office. When a developer needs to be remote, the habits that we’ve picked up during lockdown will serve us well. The flexibility offered by properly supporting remote work is undeniable, and we’ll be all the better at that going forward. But for certain things, I think that an in-person meeting will still be preferred, if it’s possible.\n","permalink":"https://engineering.resolvergroup.com/2020/05/hiring-during-a-pandemic/","summary":"\u003cp\u003eRemote working isn’t a new thing for developers; at Resolver we were already doing it regularly. Our meetings were remote-friendly, if not fully remote, and Slack would keep us all constantly connected. So when we went into lockdown in March, the transition to home-working was perhaps easier for our team than for others. But when it came to hiring new developers, removing any face-to-face contact posed some new challenges.\u003c/p\u003e\n\u003ch2 id=\"networking-events\"\u003eNetworking events\u003c/h2\u003e\n\u003cp\u003eThe hiring process would often begin by speaking at events such as \u003ca href=\"https://lrug.org/\"\u003eLRUG\u003c/a\u003e, giving a summary of \u003ca href=\"https://www.resolver.co.uk/\"\u003eResolver\u003c/a\u003e and what we’re striving for. That gave us an opportunity to meet any interested candidates straight away and to make those important first impressions. Under lockdown, we’ve had to approximate that by posting to mailing lists, posting to \u003ca href=\"https://www.linkedin.com/company/resolvergroup/\"\u003eLinkedIn\u003c/a\u003e and using recruiters. We’ve seen some good results doing that, and the market for Ruby developers still seems to be strong. But it can also be easy for the message to get lost in the noise. Emphasising what sets Resolver apart as a great place for developers has been more important than ever.\u003c/p\u003e","title":"Hiring during a pandemic"},{"content":"I finished Makers, a software development bootcamp, on Friday 13th March (yes, the irony did resonate with me as I wrote that…!) Traditionally, on the last day of the course, students show off their final projects at Demo Day. However, as we were making the final touches to our projects, we were given the news that the event couldn’t go ahead and would instead take place remotely. I think that was the moment of realisation for myself and others in my cohort at the reality of the destructive virus that was unfolding.\nCertain life events can be scary. Quitting a career to learn to code was scary. Trying to find a job as a Junior Developer with no commercial experience is scary. Doing this amongst a global pandemic seemed impossible.\nThe same week, I attended an LRUG meeting which is where I met Adam, a Lead Developer at Resolver. He told me about the company, that they had a vacancy for a Junior Developer and we swapped contact details. I sent across my CV and in turn a tech test - a Ruby test to develop a poker hand analyser, whereby a series of tests had been written and my role was to make them pass. Shortly after, I was delighted to see that the team were impressed with my test and invited me to an (virtual!) interview. During the interview, Adam and another developer asked me a series of questions about my tech test and myself and my journey into coding. I then spoke to Sofia, a Product Manager, who asked questions about why I wanted to join the company, my interest in coding and how I would feel about working remotely due to Covid-19.\nI remember my excitement as I finished the interview. I felt that it went well and also that it had been a pleasant experience and those in the call seemed warm and friendly. My gut instinct had been correct and not too long after I received the delightful news that I had been successful!\nSeveral weeks later, the night before my first day in my virtual office (aka the dining table) I had a MacBook arrive. The next morning, the IT team set it up remotely and I joined several meetings and met my colleagues via Google Hangouts. Another junior developer, Alex (who had also been to Makers), joined the same day and we were both given the opportunity to spend the first few weeks completing a course for Rails 6. During the course, I built a movie review app. It was invaluable and explained concepts in an engaging, hands-on way and proved an excellent refresher for several concepts covered at Makers.\nHaving spoken to several other Makers alumni, it seems that I have been incredibly lucky with my first few weeks as a junior developer, I heard stories of on day two, new starters being asked to write a new command line app by essentially winging it and reading API docs!\nMy learning throughout the course has been self-led, however the few times that I did have questions, I reached out on Slack to other developers in my team who were happy to jump onto a Hangouts call and answer any questions. I spent the large majority of my time during the first few weeks working my way through the course and I also joined in with any relevant meetings. Despite being remote, I already feel a part of the team which is helped by regular communications. These take place through several Google Hangouts calls:\nDaily coffee with the development team for general chat, gaming and food updates Catch up with development team and project manager twice weekly to check in on what we’ve been working on Weekly planning with development team and project manager to set weekly priorities Whole company update once a week Training catch-up once a week (we spend Friday afternoons learning something new and update the team with what we have learnt) Now that I’ve finished the course, the plan is to start pairing with senior developers on tickets and to do this until I’m more familiar and comfortable with the codebase. When that happens, I’ll begin to work on simple tickets alone but with the comfort that my colleagues are only a call away if I do need a hand or to pair.\nI feel that despite having only met Adam in person, I am beginning to form connections with my team due to our coffee chats and incredible GIF usage on Slack. We have had a few ‘after work drinks’ video calls, where I’ve discovered my colleagues\u0026rsquo; love for whiskey and our engineering manager’s dart-throwing skills. The process has completely surpassed my expectations and I am enjoying each day. Looking forward, when the day does arrive that we can safely go back into work, I am excited to see our office (I’ve heard it’s great!), to see how excited my dog is when she realises that she gets to come to work with me (and to see my dog loving colleagues’ reaction when they see my dog!) and for the human interaction with my colleagues. And of course, a trip to our local pub!\n","permalink":"https://engineering.resolvergroup.com/2020/05/joining-resolver-as-a-junior-developer-during-covid-19/","summary":"\u003cp\u003eI finished \u003ca href=\"https://makers.tech/become/makers-academy/\"\u003eMakers\u003c/a\u003e, a software development bootcamp, on Friday 13th March (yes, the irony did resonate with me as I wrote that…!) Traditionally, on the last day of the course, students show off their final projects at Demo Day. However, as we were making the final touches to our projects, we were given the news that the event couldn’t go ahead and would instead take place remotely. I think that was the moment of realisation for myself and others in my cohort at the reality of the destructive virus that was unfolding.\u003c/p\u003e","title":"Joining Resolver as a Junior Developer during Covid-19"},{"content":"Welcome to the blog for the Resolver Engineering team.\nWe spend our time building and maintaining the applications that power Resolver.co.uk as well as both SaaS and bespoke products for regulators, ombudsmen and alternative dispute resolution providers.\nOver the course of building these products, we have developed our ways of working, found tips and tricks and worked around known and unknown issues in all the products we either build or use.\nThis blog is our way of giving back to the developer community at large by writing about these things and more. A lot of the posts will be technical in nature and centered around a single product, feature or issue. But we’ll also strive to bring you the voices of our engineering team in other ways by them writing about their experiences of working at Resolver, their backgrounds, their development journeys and more.\nWe hope you enjoy reading our posts and find them helpful and useful.\n","permalink":"https://engineering.resolvergroup.com/about/","summary":"\u003cp\u003eWelcome to the blog for the Resolver Engineering team.\u003c/p\u003e\n\u003cp\u003eWe spend our time building and maintaining the applications that power Resolver.co.uk as well as both SaaS and bespoke products for regulators, ombudsmen and alternative dispute resolution providers.\u003c/p\u003e\n\u003cp\u003eOver the course of building these products, we have developed our ways of working, found tips and tricks and worked around known and unknown issues in all the products we either build or use.\u003c/p\u003e","title":"A peek into the world of Resolver Engineering"}]